From 29b0c3e73f3d1b0eacacfe5c907bd5ea788231ea Mon Sep 17 00:00:00 2001 From: deepshekhardas Date: Mon, 25 May 2026 16:58:48 +0530 Subject: [PATCH] fix: add pending_password_set claim for invite/recovery sessions 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 --- internal/api/user.go | 7 +++++++ internal/tokens/service.go | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/internal/api/user.go b/internal/api/user.go index da74c402f..9e47440ef 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -92,6 +92,13 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { user := getUser(ctx) session := getSession(ctx) + claims := getClaims(ctx) + + if claims != nil && claims.PendingPasswordSet { + if params.Email != "" || params.Phone != "" || params.Data != nil || params.AppData != nil { + return apierrors.NewForbiddenError(apierrors.ErrorCodeUserNotAllowed, "Session requires password to be set before updating other fields") + } + } if err := a.validateUserUpdateParams(ctx, params); err != nil { return err diff --git a/internal/tokens/service.go b/internal/tokens/service.go index 640847496..c50d51686 100644 --- a/internal/tokens/service.go +++ b/internal/tokens/service.go @@ -81,6 +81,7 @@ type AccessTokenClaims struct { AuthenticationMethodReference AMRClaim `json:"amr,omitempty"` SessionId string `json:"session_id,omitempty"` IsAnonymous bool `json:"is_anonymous"` + PendingPasswordSet bool `json:"pending_password_set,omitempty"` ClientID string `json:"client_id,omitempty"` Scope string `json:"scope,omitempty"` } @@ -694,6 +695,10 @@ func (s *Service) GenerateAccessToken(r *http.Request, tx *storage.Connection, p scopes = *session.Scopes } + userHasPassword := params.User.HasPassword() + isPendingPasswordSet := session.IsRecovery() || + (!userHasPassword && params.User.InvitedAt != nil) + claims := &v0hooks.AccessTokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ Subject: params.User.ID.String(), @@ -711,6 +716,7 @@ func (s *Service) GenerateAccessToken(r *http.Request, tx *storage.Connection, p AuthenticatorAssuranceLevel: aal.String(), AuthenticationMethodReference: amr, IsAnonymous: params.User.IsAnonymous, + PendingPasswordSet: isPendingPasswordSet, ClientID: clientID, Scope: scopes, }