From 4252380c3641435cc17acc0d3a7165758d044090 Mon Sep 17 00:00:00 2001 From: Raajhesh Kannaa Chidambaram <495042+raajheshkannaa@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:55:47 -0400 Subject: [PATCH] fix: support space-separated OIDC prompt parameter values The OIDC spec allows multiple prompt values separated by spaces (e.g. "select_account consent"). The validator already handled this correctly by splitting on spaces, but GenerateIDToken in strategy_jwt.go used a switch statement on the raw unsplit string, causing prompt values like "login consent" to skip login-specific validation. Split the prompt parameter by space in GenerateIDToken and use slices.Contains to check for individual values, consistent with how the validator and consent strategy already handle it. Fixes ory/hydra#4039 --- fosite/handler/openid/strategy_jwt.go | 12 +++++---- fosite/handler/openid/strategy_jwt_test.go | 30 ++++++++++++++++++++++ fosite/handler/openid/validator_test.go | 28 ++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/fosite/handler/openid/strategy_jwt.go b/fosite/handler/openid/strategy_jwt.go index 38b7e9fa898..f4bfb6f881c 100644 --- a/fosite/handler/openid/strategy_jwt.go +++ b/fosite/handler/openid/strategy_jwt.go @@ -5,7 +5,9 @@ package openid import ( "context" + "slices" "strconv" + "strings" "time" "github.com/ory/x/errorsx" @@ -157,20 +159,20 @@ func (h DefaultStrategy) GenerateIDToken(ctx context.Context, lifespan time.Dura } } - prompt := requester.GetRequestForm().Get("prompt") - if prompt != "" { + prompts := fosite.RemoveEmpty(strings.Split(requester.GetRequestForm().Get("prompt"), " ")) + if len(prompts) > 0 { if claims.AuthTime.IsZero() { return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Unable to determine validity of prompt parameter because auth_time is missing in id token claims.")) } } - switch prompt { - case "none": + if slices.Contains(prompts, "none") { if !claims.AuthTime.Equal(claims.RequestedAt) && claims.AuthTime.After(claims.RequestedAt) { return "", errorsx.WithStack(fosite.ErrServerError. WithDebugf("Failed to generate id token because prompt was set to 'none' but auth_time ('%s') happened after the authorization request ('%s') was registered, indicating that the user was logged in during this request which is not allowed.", claims.AuthTime, claims.RequestedAt)) } - case "login": + } + if slices.Contains(prompts, "login") { if !claims.AuthTime.Equal(claims.RequestedAt) && claims.AuthTime.Before(claims.RequestedAt) { return "", errorsx.WithStack(fosite.ErrServerError. WithDebugf("Failed to generate id token because prompt was set to 'login' but auth_time ('%s') happened before the authorization request ('%s') was registered, indicating that the user was not re-authenticated which is forbidden.", claims.AuthTime, claims.RequestedAt)) diff --git a/fosite/handler/openid/strategy_jwt_test.go b/fosite/handler/openid/strategy_jwt_test.go index be0edd98fc1..503410ffefc 100644 --- a/fosite/handler/openid/strategy_jwt_test.go +++ b/fosite/handler/openid/strategy_jwt_test.go @@ -213,6 +213,36 @@ func TestJWTStrategy_GenerateIDToken(t *testing.T) { }, expectErr: true, }, + { + description: "should pass because prompt=select_account consent is valid space-separated per OIDC spec", + setup: func() { + req = fosite.NewAccessRequest(&openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().Add(-time.Hour).UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("prompt", "select_account consent") + }, + expectErr: false, + }, + { + description: "should fail because prompt includes login in space-separated list and auth_time indicates old login", + setup: func() { + req = fosite.NewAccessRequest(&openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().Add(-time.Hour).UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("prompt", "login consent") + }, + expectErr: true, + }, { description: "should pass because id_token_hint subject matches subject from claims", setup: func() { diff --git a/fosite/handler/openid/validator_test.go b/fosite/handler/openid/validator_test.go index dbdac9eec42..11cfcb58519 100644 --- a/fosite/handler/openid/validator_test.go +++ b/fosite/handler/openid/validator_test.go @@ -178,6 +178,34 @@ func TestValidatePrompt(t *testing.T) { }, }, }, + { + d: "should pass because select_account consent is a valid space-separated prompt per OIDC spec", + prompt: "select_account consent", + isPublic: false, + expectErr: false, + s: &openid.DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Minute), + }, + }, + }, + { + d: "should pass because select_account consent works with public clients", + prompt: "select_account consent", + isPublic: true, + expectErr: false, + s: &openid.DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Minute), + }, + }, + }, { d: "should pass because requesting consent and login works with confidential clients", prompt: "login consent",