mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:17:32 +00:00
feat: specify login UI version on instance and apps (#9071)
# Which Problems Are Solved To be able to migrate or test the new login UI, admins might want to (temporarily) switch individual apps. At a later point admin might want to make sure all applications use the new login UI. # How the Problems Are Solved - Added a feature flag `` on instance level to require all apps to use the new login and provide an optional base url. - if the flag is enabled, all (OIDC) applications will automatically use the v2 login. - if disabled, applications can decide based on their configuration - Added an option on OIDC apps to use the new login UI and an optional base url. - Removed the requirement to use `x-zitadel-login-client` to be redirected to the login V2 and retrieve created authrequest and link them to SSO sessions. - Added a new "IAM_LOGIN_CLIENT" role to allow management of users, sessions, grants and more without `x-zitadel-login-client`. # Additional Changes None # Additional Context closes https://github.com/zitadel/zitadel/issues/8702
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -26,7 +28,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
LoginClientHeader = "x-zitadel-login-client"
|
||||
LoginClientHeader = "x-zitadel-login-client"
|
||||
LoginPostLogoutRedirectParam = "post_logout_redirect"
|
||||
LoginPath = "/login"
|
||||
LogoutPath = "/logout"
|
||||
LogoutDonePath = "/logout/done"
|
||||
)
|
||||
|
||||
func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest, userID string) (_ op.AuthRequest, err error) {
|
||||
@@ -36,12 +42,34 @@ func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest
|
||||
span.EndWithError(err)
|
||||
}()
|
||||
|
||||
// for backwards compatibility we pass the login client if set
|
||||
headers, _ := http_utils.HeadersFromCtx(ctx)
|
||||
if loginClient := headers.Get(LoginClientHeader); loginClient != "" {
|
||||
loginClient := headers.Get(LoginClientHeader)
|
||||
|
||||
// if the instance requires the v2 login, use it no matter what the application configured
|
||||
if authz.GetFeatures(ctx).LoginV2.Required {
|
||||
return o.createAuthRequestLoginClient(ctx, req, userID, loginClient)
|
||||
}
|
||||
|
||||
return o.createAuthRequest(ctx, req, userID)
|
||||
version, err := o.query.OIDCClientLoginVersion(ctx, req.ClientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch version {
|
||||
case domain.LoginVersion1:
|
||||
return o.createAuthRequest(ctx, req, userID)
|
||||
case domain.LoginVersion2:
|
||||
return o.createAuthRequestLoginClient(ctx, req, userID, loginClient)
|
||||
case domain.LoginVersionUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
// if undefined, use the v2 login if the header is sent, to retain the current behavior
|
||||
if loginClient != "" {
|
||||
return o.createAuthRequestLoginClient(ctx, req, userID, loginClient)
|
||||
}
|
||||
return o.createAuthRequest(ctx, req, userID)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OPStorage) createAuthRequestScopeAndAudience(ctx context.Context, clientID string, reqScope []string) (scope, audience []string, err error) {
|
||||
@@ -240,18 +268,35 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
|
||||
|
||||
// check for the login client header
|
||||
headers, _ := http_utils.HeadersFromCtx(ctx)
|
||||
// in case there is no id_token_hint, redirect to the UI and let it decide which session to terminate
|
||||
if headers.Get(LoginClientHeader) != "" && endSessionRequest.IDTokenHintClaims == nil {
|
||||
return o.defaultLogoutURLV2 + endSessionRequest.RedirectURI, nil
|
||||
|
||||
// V2:
|
||||
// In case there is no id_token_hint and login V2 is either required by feature
|
||||
// or requested via header (backwards compatibility),
|
||||
// we'll redirect to the UI (V2) and let it decide which session to terminate
|
||||
//
|
||||
// If there's no id_token_hint and for v1 logins, we handle them separately
|
||||
if endSessionRequest.IDTokenHintClaims == nil &&
|
||||
(authz.GetFeatures(ctx).LoginV2.Required || headers.Get(LoginClientHeader) != "") {
|
||||
redirectURI := v2PostLogoutRedirectURI(endSessionRequest.RedirectURI)
|
||||
// if no base uri is set, fallback to the default configured in the runtime config
|
||||
if authz.GetFeatures(ctx).LoginV2.BaseURI == nil || authz.GetFeatures(ctx).LoginV2.BaseURI.String() == "" {
|
||||
return o.defaultLogoutURLV2 + redirectURI, nil
|
||||
}
|
||||
return buildLoginV2LogoutURL(authz.GetFeatures(ctx).LoginV2.BaseURI, redirectURI), nil
|
||||
}
|
||||
|
||||
// If there is no login client header and no id_token_hint or the id_token_hint does not have a session ID,
|
||||
// do a v1 Terminate session (which terminates all sessions of the user agent, identified by cookie).
|
||||
// V1:
|
||||
// We check again for the id_token_hint param and if a session is set in it.
|
||||
// All explicit V2 sessions with empty id_token_hint are handled above and all V2 session contain a sessionID
|
||||
// So if any condition is not met, we handle the request as a V1 request and do a (v1) TerminateSession,
|
||||
// which terminates all sessions of the user agent, identified by cookie.
|
||||
if endSessionRequest.IDTokenHintClaims == nil || endSessionRequest.IDTokenHintClaims.SessionID == "" {
|
||||
return endSessionRequest.RedirectURI, o.TerminateSession(ctx, endSessionRequest.UserID, endSessionRequest.ClientID)
|
||||
}
|
||||
|
||||
// If the sessionID is prefixed by V1, we also terminate a v1 session.
|
||||
// V1:
|
||||
// If the sessionID is prefixed by V1, we also terminate a v1 session, but based on the SingleV1SessionTermination feature flag,
|
||||
// we either terminate all sessions of the user agent or only the specific session
|
||||
if strings.HasPrefix(endSessionRequest.IDTokenHintClaims.SessionID, handler.IDPrefixV1) {
|
||||
err = o.terminateV1Session(ctx, endSessionRequest.UserID, endSessionRequest.IDTokenHintClaims.SessionID)
|
||||
if err != nil {
|
||||
@@ -260,12 +305,31 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
|
||||
return endSessionRequest.RedirectURI, nil
|
||||
}
|
||||
|
||||
// terminate the v2 session of the id_token_hint
|
||||
// V2:
|
||||
// Terminate the v2 session of the id_token_hint
|
||||
_, err = o.command.TerminateSessionWithoutTokenCheck(ctx, endSessionRequest.IDTokenHintClaims.SessionID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return endSessionRequest.RedirectURI, nil
|
||||
return v2PostLogoutRedirectURI(endSessionRequest.RedirectURI), nil
|
||||
}
|
||||
|
||||
func buildLoginV2LogoutURL(baseURI *url.URL, redirectURI string) string {
|
||||
baseURI.JoinPath(LogoutPath)
|
||||
q := baseURI.Query()
|
||||
q.Set(LoginPostLogoutRedirectParam, redirectURI)
|
||||
baseURI.RawQuery = q.Encode()
|
||||
return baseURI.String()
|
||||
}
|
||||
|
||||
// v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins.
|
||||
// The default value set by the [op.SessionEnder] only handles V1 logins. In case the redirect_uri is set to the default
|
||||
// we'll return the path for the v2 login.
|
||||
func v2PostLogoutRedirectURI(redirectURI string) string {
|
||||
if redirectURI != login.DefaultLoggedOutPath {
|
||||
return redirectURI
|
||||
}
|
||||
return LogoutDonePath
|
||||
}
|
||||
|
||||
// terminateV1Session terminates "v1" sessions created through the login UI.
|
||||
|
Reference in New Issue
Block a user