mirror of
https://github.com/zitadel/zitadel.git
synced 2025-04-22 10:01:40 +00:00

# Which Problems Are Solved Potential nil pointers leading to a panic in the login UI. # How the Problems Are Solved As of now the login UI did not always check if the authRequest was actually retrieved form the database, which is ok for some endpoints which can also be called outside of an auth request. There are now methods added to ensure the request is loaded. # Additional Changes None # Additional Context Closes https://github.com/zitadel/DevOps/issues/55
194 lines
6.8 KiB
Go
194 lines
6.8 KiB
Go
package login
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/muhlemmer/gu"
|
|
"github.com/zitadel/logging"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
)
|
|
|
|
const (
|
|
tmplDeviceAuthUserCode = "device-usercode"
|
|
tmplDeviceAuthAction = "device-action"
|
|
)
|
|
|
|
func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, err error) {
|
|
var errID, errMessage string
|
|
if err != nil {
|
|
logging.WithError(err).Error()
|
|
errID, errMessage = l.getErrorMessage(r, err)
|
|
}
|
|
translator := l.getTranslator(r.Context(), nil)
|
|
data := l.getBaseData(r, nil, translator, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
|
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil)
|
|
}
|
|
|
|
func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) {
|
|
translator := l.getTranslator(r.Context(), authReq)
|
|
data := &struct {
|
|
baseData
|
|
AuthRequestID string
|
|
Username string
|
|
ClientID string
|
|
Scopes []string
|
|
}{
|
|
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
|
|
AuthRequestID: authReq.ID,
|
|
Username: authReq.UserName,
|
|
ClientID: authReq.ApplicationID,
|
|
Scopes: scopes,
|
|
}
|
|
|
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil)
|
|
}
|
|
|
|
const (
|
|
deviceAuthAllowed = "allowed"
|
|
deviceAuthDenied = "denied"
|
|
)
|
|
|
|
// renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied.
|
|
func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) {
|
|
translator := l.getTranslator(r.Context(), authReq)
|
|
data := &struct {
|
|
baseData
|
|
Message string
|
|
}{
|
|
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
|
|
}
|
|
switch action {
|
|
case deviceAuthAllowed:
|
|
data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil)
|
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplSuccess], data, nil)
|
|
case deviceAuthDenied:
|
|
data.ErrMessage = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Denied", nil)
|
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil)
|
|
}
|
|
}
|
|
|
|
// handleDeviceAuthUserCode serves the Device Authorization user code submission form.
|
|
// The "user_code" may be submitted by URL (GET) or form (POST).
|
|
// When a "user_code" is received and found through query,
|
|
// handleDeviceAuthUserCode will create a new AuthRequest in the repository.
|
|
// The user is then redirected to the /login endpoint to complete authentication.
|
|
//
|
|
// The agent ID from the context is set to the authentication request
|
|
// to ensure the complete login flow is completed from the same browser.
|
|
func (l *Login) handleDeviceAuthUserCode(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
l.renderDeviceAuthUserCode(w, r, err)
|
|
return
|
|
}
|
|
userCode := r.Form.Get("user_code")
|
|
if userCode == "" {
|
|
if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" {
|
|
err = errors.New(prompt)
|
|
}
|
|
l.renderDeviceAuthUserCode(w, r, err)
|
|
return
|
|
}
|
|
deviceAuthReq, err := l.query.DeviceAuthRequestByUserCode(ctx, userCode)
|
|
if err != nil {
|
|
l.renderDeviceAuthUserCode(w, r, err)
|
|
return
|
|
}
|
|
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
|
if !ok {
|
|
l.renderDeviceAuthUserCode(w, r, errors.New("internal error: agent ID missing"))
|
|
return
|
|
}
|
|
authRequest, err := l.authRepo.CreateAuthRequest(ctx, &domain.AuthRequest{
|
|
CreationDate: time.Now(),
|
|
BrowserInfo: domain.BrowserInfoFromRequest(r),
|
|
AgentID: userAgentID,
|
|
ApplicationID: deviceAuthReq.ClientID,
|
|
InstanceID: authz.GetInstance(ctx).InstanceID(),
|
|
Request: deviceAuthReq,
|
|
})
|
|
if err != nil {
|
|
l.renderDeviceAuthUserCode(w, r, err)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, l.renderer.pathPrefix+EndpointLogin+"?authRequestID="+authRequest.ID, http.StatusFound)
|
|
}
|
|
|
|
// redirectDeviceAuthStart redirects the user to the start point of
|
|
// the device authorization flow. A prompt can be set to inform the user
|
|
// of the reason why they are redirected back.
|
|
func (l *Login) redirectDeviceAuthStart(w http.ResponseWriter, r *http.Request, prompt string) {
|
|
values := make(url.Values)
|
|
values.Set("prompt", url.QueryEscape(prompt))
|
|
|
|
url := url.URL{
|
|
Path: l.renderer.pathPrefix + EndpointDeviceAuth,
|
|
RawQuery: values.Encode(),
|
|
}
|
|
http.Redirect(w, r, url.String(), http.StatusSeeOther)
|
|
}
|
|
|
|
// handleDeviceAuthAction is the handler where the user is redirected after login.
|
|
// The authRequest is checked if the login was indeed completed.
|
|
// When the action of "allowed" or "denied", the device authorization is updated accordingly.
|
|
// Else the user is presented with a page where they can choose / submit either action.
|
|
func (l *Login) handleDeviceAuthAction(w http.ResponseWriter, r *http.Request) {
|
|
authReq, err := l.ensureAuthRequest(r)
|
|
if err != nil {
|
|
l.redirectDeviceAuthStart(w, r, err.Error())
|
|
return
|
|
}
|
|
if !authReq.Done() {
|
|
l.redirectDeviceAuthStart(w, r, "authentication not completed")
|
|
return
|
|
}
|
|
authDev, ok := authReq.Request.(*domain.AuthRequestDevice)
|
|
if !ok {
|
|
l.redirectDeviceAuthStart(w, r, fmt.Sprintf("wrong auth request type: %T", authReq.Request))
|
|
return
|
|
}
|
|
|
|
action := mux.Vars(r)["action"]
|
|
switch action {
|
|
case deviceAuthAllowed:
|
|
_, err = l.command.ApproveDeviceAuth(r.Context(), authDev.DeviceCode, authReq.UserID, authReq.UserOrgID, authReq.UserAuthMethodTypes(), authReq.AuthTime, authReq.PreferredLanguage, authReq.BrowserInfo.ToUserAgent())
|
|
case deviceAuthDenied:
|
|
_, err = l.command.CancelDeviceAuth(r.Context(), authDev.DeviceCode, domain.DeviceAuthCanceledDenied)
|
|
default:
|
|
l.renderDeviceAuthAction(w, r, authReq, authDev.Scopes)
|
|
return
|
|
}
|
|
if err != nil {
|
|
l.redirectDeviceAuthStart(w, r, err.Error())
|
|
return
|
|
}
|
|
|
|
l.renderDeviceAuthDone(w, r, authReq, action)
|
|
}
|
|
|
|
// deviceAuthCallbackURL creates the callback URL with which the user
|
|
// is redirected back to the device authorization flow.
|
|
func (l *Login) deviceAuthCallbackURL(authRequestID string) string {
|
|
return l.renderer.pathPrefix + EndpointDeviceAuthAction + "?authRequestID=" + authRequestID
|
|
}
|
|
|
|
// RedirectDeviceAuthToPrefix allows users to use https://domain.com/device without the /ui/login prefix
|
|
// and redirects them to the prefixed endpoint.
|
|
// [rfc 8628](https://www.rfc-editor.org/rfc/rfc8628#section-3.2) recommends the URL to be as short as possible.
|
|
func RedirectDeviceAuthToPrefix(w http.ResponseWriter, r *http.Request) {
|
|
target := gu.PtrCopy(r.URL)
|
|
target.Path = HandlerPrefix + EndpointDeviceAuth
|
|
http.Redirect(w, r, target.String(), http.StatusFound)
|
|
}
|