fix: add pending_password_set claim for invite/recovery sessions - #2539
fix: add pending_password_set claim for invite/recovery sessions#2539deepshekhardas wants to merge 1 commit into
Conversation
| AuthenticatorAssuranceLevel: aal.String(), | ||
| AuthenticationMethodReference: amr, | ||
| IsAnonymous: params.User.IsAnonymous, | ||
| PendingPasswordSet: isPendingPasswordSet, |
There was a problem hiding this comment.
⚪ Severity: LOW
The custom-access-token path constructs the hook-facing v0hooks.AccessTokenClaims, whose definition does not contain PendingPasswordSet (the current revision therefore does not compile). If the mismatch is resolved without propagating the field, hook-generated tokens omit the marker and pending sessions bypass the UserUpdate restriction.
Helpful? Add 👍 / 👎
💡 Fix Suggestion
Suggestion: Add the PendingPasswordSet bool field to the v0hooks.AccessTokenClaims struct in internal/hooks/v0hooks/v0hooks.go. The struct currently lacks this field (it exists in AccessTokenClaims defined within tokens/service.go but not in the hook-facing version), which causes a compile error. Add the field with a proper JSON tag after the existing fields:
PendingPasswordSet bool `json:"pending_password_set,omitempty"`Additionally, since the custom-hook path converts the struct to a map[string]any (output.Claims) and uses it directly as JWT claims, hook implementations must explicitly include pending_password_set in their returned claims map. If the hook omits it, the JWT is minted with the zero value (false) and pending invite/recovery sessions will bypass the UserUpdate restriction. Consider adding validation in the hook output processing step to ensure pending_password_set is propagated from the initial claims to the final hook output, or document that hook authors must preserve this claim.
Adds a pending_password_set claim to JWT tokens for sessions created via invite or recovery flows before the user sets their password. Enforces server-side that only password updates are allowed while this claim is present. Fixes #45210 (cherry picked from commit b56a02d)
| userHasPassword := params.User.HasPassword() | ||
| isPendingPasswordSet := session.IsRecovery() || | ||
| (!userHasPassword && params.User.InvitedAt != nil) |
There was a problem hiding this comment.
🟡 Severity: MEDIUM
An invite verification stores a random temporary password before this calculation, so HasPassword() is already true although the invitee has not chosen a password. The issued token therefore carries pending_password_set=false, allowing its holder to use UserUpdate to change email, phone, data, or app_metadata.
Helpful? Add 👍 / 👎
💡 Fix Suggestion
Suggestion: The bug occurs because signupVerify sets a temporary random password on the user before calling GenerateAccessToken, causing HasPassword() to return true and making the invite arm of the condition unreachable. The fix should also inspect the session's AMR claims for the Invite authentication method — exactly as IsRecovery() does for recovery methods. Preferred approach: add an IsInvite() method to the Session struct in internal/models/sessions.go (mirroring IsRecovery() but matching models.Invite instead) and update the condition here to session.IsRecovery() || session.IsInvite() || (!userHasPassword && params.User.InvitedAt != nil). Alternatively, inline the AMR-claim check at lines 700–702 as shown in the suggested code.
⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.
| userHasPassword := params.User.HasPassword() | |
| isPendingPasswordSet := session.IsRecovery() || | |
| (!userHasPassword && params.User.InvitedAt != nil) | |
| userHasPassword := params.User.HasPassword() | |
| isInviteSession := false | |
| for _, amrClaim := range session.AMRClaims { | |
| if am, err := models.ParseAuthenticationMethod(amrClaim.GetAuthenticationMethod()); err == nil && am == models.Invite { | |
| isInviteSession = true | |
| break | |
| } | |
| } | |
| isPendingPasswordSet := session.IsRecovery() || isInviteSession || | |
| (!userHasPassword && params.User.InvitedAt != nil) |
Changes
Adds a \pending_password_set\ claim to JWT tokens for sessions created via invite or recovery flows before the user sets their password.
Rationale
When a user is invited or goes through password recovery, the session is created via OTP/recovery verification. Before the password is set, this session should be restricted — only password updates should be allowed. RLS policies can also use the \pending_password_set\ claim to restrict data access.
Fixes #45210