mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-08 15:48:33 +00:00

# Which Problems Are Solved id_tokens issued for auth requests created through the login UI currently do not provide a sid claim. This is due to the fact that (SSO) sessions for the login UI do not have one and are only computed by the userAgent(ID), the user(ID) and the authentication checks of the latter. This prevents client to track sessions and terminate specific session on the end_session_endpoint. # How the Problems Are Solved - An `id` column is added to the `auth.user_sessions` table. - The `id` (prefixed with `V1_`) is set whenever a session is added or updated to active (from terminated) - The id is passed to the `oidc session` (as v2 sessionIDs), to expose it as `sid` claim # Additional Changes - refactored `getUpdateCols` to handle different column value types and add arguments for query # Additional Context - closes #8499 - relates to #8501
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.ToUserAgent(), authReq.SessionID)
|
|
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)
|
|
}
|