diff --git a/frontend/src/lib/hooks/screen-params.ts b/frontend/src/lib/hooks/screen-params.ts index 921fa4b6..6a513d33 100644 --- a/frontend/src/lib/hooks/screen-params.ts +++ b/frontend/src/lib/hooks/screen-params.ts @@ -6,6 +6,7 @@ type ScreenParams = { oidc_ticket?: string; oidc_scope?: string; oidc_name?: string; + oidc_login?: boolean; }; const zodScreenParams = z.object({ @@ -14,6 +15,7 @@ const zodScreenParams = z.object({ oidc_ticket: z.string().optional(), oidc_scope: z.string().optional(), oidc_name: z.string().optional(), + oidc_login: z.stringbool().optional(), }); export function useScreenParams(params: URLSearchParams): ScreenParams { diff --git a/frontend/src/pages/authorize-page.tsx b/frontend/src/pages/authorize-page.tsx index ae355be0..20e9102f 100644 --- a/frontend/src/pages/authorize-page.tsx +++ b/frontend/src/pages/authorize-page.tsx @@ -119,7 +119,7 @@ export const AuthorizePage = () => { ); } - if (!auth.authenticated) { + if (!auth.authenticated || screenParams.oidc_login) { return ; } diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index 5851b19c..86cf3a9d 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -63,7 +63,10 @@ export const LoginPage = () => { const searchParams = new URLSearchParams(search); const screenParams = useScreenParams(searchParams); - const compiledParams = recompileScreenParams(screenParams); + const compiledParams = recompileScreenParams({ + ...screenParams, + oidc_login: false, + }); const loginForUrl = useLoginFor({ login_for: screenParams.login_for, compiledParams, @@ -196,7 +199,7 @@ export const LoginPage = () => { }; }, [redirectTimer, redirectButtonTimer]); - if (auth.authenticated) { + if (auth.authenticated && !screenParams.oidc_login) { return ; } diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index a8aef39e..afa3f6ba 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -73,6 +73,7 @@ type AuthorizeScreenParams struct { OIDCTicket string `url:"oidc_ticket"` OIDCScope string `url:"oidc_scope"` OIDCName string `url:"oidc_name"` + OIDCLogin bool `url:"oidc_login"` } type AuthorizeCompleteRequest struct { @@ -169,12 +170,18 @@ func (controller *OIDCController) authorize(c *gin.Context) { ticket := controller.oidc.CreateAuthorizeRequestTicket(*req) - queries, err := query.Values(AuthorizeScreenParams{ + values := AuthorizeScreenParams{ LoginFor: FrontendLoginForOIDC, OIDCTicket: ticket, OIDCScope: req.Scope, OIDCName: client.Name, - }) + } + + if req.Prompt == "login" { + values.OIDCLogin = true + } + + queries, err := query.Values(values) if err != nil { controller.authorizeError(c, authorizeErrorParams{ @@ -425,7 +432,7 @@ func (controller *OIDCController) Token(c *gin.Context) { return } - tokenRes, err := controller.oidc.GenerateAccessToken(c, client, *entry) + tokenRes, err := controller.oidc.GenerateAccessToken(c, client, *entry, entry.AuthTime) if err != nil { controller.log.App.Error().Err(err).Msg("Failed to generate access token") diff --git a/internal/model/context.go b/internal/model/context.go index b0808568..7a4395bf 100644 --- a/internal/model/context.go +++ b/internal/model/context.go @@ -25,6 +25,7 @@ const ( type UserContext struct { Authenticated bool Provider ProviderType + AuthTime int64 Local *LocalContext OAuth *OAuthContext LDAP *LDAPContext @@ -110,6 +111,7 @@ func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) { func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext, error) { *c = UserContext{ Authenticated: !session.TotpPending, + AuthTime: session.CreatedAt, } switch session.Provider { diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go index da988c49..a64349e9 100644 --- a/internal/service/oidc_service.go +++ b/internal/service/oidc_service.go @@ -54,6 +54,7 @@ type ClaimSet struct { Sub string `json:"sub"` Iat int64 `json:"iat"` Exp int64 `json:"exp"` + AuthTime int64 `json:"auth_time,omitempty"` Name string `json:"name,omitempty"` GivenName string `json:"given_name,omitempty"` FamilyName string `json:"family_name,omitempty"` @@ -117,6 +118,7 @@ type AuthorizeRequest struct { Nonce string `form:"nonce" json:"nonce" url:"nonce"` CodeChallenge string `form:"code_challenge" json:"code_challenge" url:"code_challenge"` CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" url:"code_challenge_method"` + Prompt string `form:"prompt" json:"prompt" url:"prompt"` } type AuthorizeCodeEntry struct { @@ -127,6 +129,7 @@ type AuthorizeCodeEntry struct { Nonce string CodeChallenge string Userinfo UserinfoResponse + AuthTime int64 } type UsedCodeEntry struct { @@ -423,6 +426,7 @@ func (service *OIDCService) CreateCode(req AuthorizeRequest, userContext model.U ClientID: req.ClientID, Nonce: req.Nonce, Userinfo: service.userinfoFromContext(userContext, sub), + AuthTime: userContext.AuthTime, } if req.CodeChallenge != "" { @@ -512,7 +516,7 @@ func (service *OIDCService) GetCodeEntry(codeHash string, clientId string) (*Aut return &entry, true } -func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user UserinfoResponse, scope string, nonce string) (string, error) { +func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user UserinfoResponse, scope string, nonce string, auth_time int64) (string, error) { createdAt := time.Now().Unix() expiresAt := time.Now().Add(time.Duration(service.config.Auth.SessionExpiry) * time.Second).Unix() @@ -549,6 +553,7 @@ func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user Sub: user.Sub, Iat: createdAt, Exp: expiresAt, + AuthTime: auth_time, Name: userInfo.Name, Email: userInfo.Email, EmailVerified: userInfo.EmailVerified, @@ -578,8 +583,8 @@ func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user return token, nil } -func (service *OIDCService) GenerateAccessToken(ctx context.Context, client model.OIDCClientConfig, codeEntry AuthorizeCodeEntry) (*TokenResponse, error) { - idToken, err := service.generateIDToken(client, codeEntry.Userinfo, codeEntry.Scope, codeEntry.Nonce) +func (service *OIDCService) GenerateAccessToken(ctx context.Context, client model.OIDCClientConfig, codeEntry AuthorizeCodeEntry, authTime int64) (*TokenResponse, error) { + idToken, err := service.generateIDToken(client, codeEntry.Userinfo, codeEntry.Scope, codeEntry.Nonce, authTime) if err != nil { return nil, err @@ -660,7 +665,7 @@ func (service *OIDCService) RefreshAccessToken(ctx context.Context, refreshToken idToken, err := service.generateIDToken(model.OIDCClientConfig{ ClientID: entry.ClientID, - }, userInfo, entry.Scope, entry.Nonce) + }, userInfo, entry.Scope, entry.Nonce, 0) // auth_time is not available during refresh, so we set it to 0 if err != nil { return nil, err @@ -929,5 +934,6 @@ func (service *OIDCService) DecodeAuthorizeJWT(tokenString string) (*AuthorizeRe Nonce: get("nonce"), CodeChallenge: get("code_challenge"), CodeChallengeMethod: get("code_challenge_method"), + Prompt: get("prompt"), }, nil }