Skip to content

fix: add pending_password_set claim for invite/recovery sessions - #2539

Open
deepshekhardas wants to merge 1 commit into
supabase:masterfrom
deepshekhardas:fix/pending-password-set-claim
Open

fix: add pending_password_set claim for invite/recovery sessions#2539
deepshekhardas wants to merge 1 commit into
supabase:masterfrom
deepshekhardas:fix/pending-password-set-claim

Conversation

@deepshekhardas

Copy link
Copy Markdown

Changes

Adds a \pending_password_set\ claim to JWT tokens for sessions created via invite or recovery flows before the user sets their password.

  • **\ okens/service.go**: Added \PendingPasswordSet bool\ to \AccessTokenClaims. In \GenerateAccessToken(), sets \PendingPasswordSet = true\ when the session is a recovery session or when the user has no password and was invited.
  • **\�pi/user.go**: In \UserUpdate(), checks \claims.PendingPasswordSet\ and rejects updates to email, phone, data, or app_metadata until the password is set.

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

Comment thread internal/tokens/service.go
AuthenticatorAssuranceLevel: aal.String(),
AuthenticationMethodReference: amr,
IsAnonymous: params.User.IsAnonymous,
PendingPasswordSet: isPendingPasswordSet,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Comment on lines +700 to +702
userHasPassword := params.User.HasPassword()
isPendingPasswordSet := session.IsRecovery() ||
(!userHasPassword && params.User.InvitedAt != nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant