Description
The delegation:<username> mapper (ParameterizedScopeUserPropertyMapper) re-resolves the target user by username on every subject token refresh. If the target admin user is deleted and a new user is created with the same username within the subject user's session lifetime, the refreshed subject token emits may_act.sub for the replacement user - transferring the delegation consent to an unrelated identity without re-consent.
Value Proposition
The delegation scope uses IS_ALWAYS_CONSENT, so consent is never persisted to the database — it lives only as a session-scoped client note (session_consent). This bounds the attack window to the subject user's session lifetime: the target admin must be deleted and recreated with the same username while the subject's session is still active, and the new user must obtain their own actor token to perform the exchange.
Despite the narrow window, the impact is privilege escalation - the new user silently inherits delegation authority over the subject without the subject ever re-consenting. Since may_act.sub contains the re-resolved UUID and validateMayAct() matches it against the new user's actor token, the exchange succeeds.
Goals
- Pin the resolved target user's internal UUID in a client session note (e.g.
delegation.pinned_user_id) at first resolution
- On subject token refresh, verify the username still resolves to the same UUID - if mismatched, skip the
may_act claim
- Handle pre-existing sessions without the note gracefully (store current value on first encounter)
- Apply the same fix to
ParameterizedScopeClientSubMapper for delegation:client:<client-id>
- Test coverage for delete-and-recreate within a session, normal refresh, and pre-migration upgrade
Non-Goals
- Changing
TokenExchangeDelegationProvider.validateMayAct() or the ParameterizedScopeTypeProvider SPI
- Persisting delegation consent to the database — the
IS_ALWAYS_CONSENT design is intentional
- Addressing username mutability in general
Discussion
Identified in PR #51306 review. Parent issue: #50204.
Notes
The fix lives in the mapper layer. setClaim() receives UserSessionModel, which provides access to client session notes — no SPI changes needed. The consent itself is session-scoped (never persisted to DB), so the pinned UUID shares the same lifecycle as the consent.
Description
The
delegation:<username>mapper (ParameterizedScopeUserPropertyMapper) re-resolves the target user by username on every subject token refresh. If the target admin user is deleted and a new user is created with the same username within the subject user's session lifetime, the refreshed subject token emitsmay_act.subfor the replacement user - transferring the delegation consent to an unrelated identity without re-consent.Value Proposition
The
delegationscope usesIS_ALWAYS_CONSENT, so consent is never persisted to the database — it lives only as a session-scoped client note (session_consent). This bounds the attack window to the subject user's session lifetime: the target admin must be deleted and recreated with the same username while the subject's session is still active, and the new user must obtain their own actor token to perform the exchange.Despite the narrow window, the impact is privilege escalation - the new user silently inherits delegation authority over the subject without the subject ever re-consenting. Since
may_act.subcontains the re-resolved UUID andvalidateMayAct()matches it against the new user's actor token, the exchange succeeds.Goals
delegation.pinned_user_id) at first resolutionmay_actclaimParameterizedScopeClientSubMapperfordelegation:client:<client-id>Non-Goals
TokenExchangeDelegationProvider.validateMayAct()or theParameterizedScopeTypeProviderSPIIS_ALWAYS_CONSENTdesign is intentionalDiscussion
Identified in PR #51306 review. Parent issue: #50204.
Notes
The fix lives in the mapper layer.
setClaim()receivesUserSessionModel, which provides access to client session notes — no SPI changes needed. The consent itself is session-scoped (never persisted to DB), so the pinned UUID shares the same lifecycle as the consent.