fix(oidc): persist original auth_time on refresh family; re-intersect scope at rotation #287
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/bunyip!287
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/BUNYIP-262-refresh-auth-time-scope"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
BUNYIP-262: two related polish items on the refresh-token rotation hot path.
auth_time honesty.
handle_refresh_grantpreviously stampedauth_time = Utc::now()on every rotated at+jwt, which violates OIDC Core §2 ("the time when the End-User authentication occurred"). A stale family that's been refreshed every hour for a year appeared "freshly authenticated" to any RP doing step-up auth onauth_timedeltas. Add arefresh_token_families.auth_timecolumn (migration20260701000010), populate it on the initial code-to-token exchange via the newissue_refresh_token(auth_time, ...)parameter, JOIN it into the rotation'sRefreshTokenRotationRow(FOR UPDATE OF rt, not the family), carry it onRotatedTokens, and feed it back intomint_access_tokenathandle_refresh_grant. Existing rows backfill to NOW() because the original auth_time is unrecoverable; the next rotation onwards reports the truthful value.Scope re-intersection. The rotation previously kept the original scope set (narrowed only by
requested_scopewhen present) and ignored the user's currentuser_application_access.granted_scopes. If an admin revoked a scope between the original code redemption and the refresh, the rotated at+jwt still carried the wider scope set until the family expired naturally; the audit named this medium-severity. Add a SELECT inside the rotation transaction that re-intersectseffective_scopewith the user's still-granted set (revoked_at IS NULL). When the intersection is empty the rotation fails withinvalid_grant; when it narrowed, the at+jwt is reminted with the narrower set.Both changes are wholly internal to the OIDC vertical; no RP integration changes, no
.sqlx/offline-cache regeneration (the new column lands behind runtime queries), no schema impact onrefresh_tokens_v2. Reuse detection and family revocation paths untouched.#BUNYIP-262
BUNYIP-262: two related polish items on the refresh-token rotation hot path. auth_time honesty. `handle_refresh_grant` previously stamped `auth_time = Utc::now()` on every rotated at+jwt, which violates OIDC Core §2 ("the time when the End-User authentication occurred"). A stale family that's been refreshed every hour for a year appeared "freshly authenticated" to any RP doing step-up auth on `auth_time` deltas. Add a `refresh_token_families.auth_time` column (migration `20260701000010`), populate it on the initial code-to-token exchange via the new `issue_refresh_token(auth_time, ...)` parameter, JOIN it into the rotation's `RefreshTokenRotationRow` (FOR UPDATE OF rt, not the family), carry it on `RotatedTokens`, and feed it back into `mint_access_token` at `handle_refresh_grant`. Existing rows backfill to NOW() because the original auth_time is unrecoverable; the next rotation onwards reports the truthful value. Scope re-intersection. The rotation previously kept the original scope set (narrowed only by `requested_scope` when present) and ignored the user's current `user_application_access.granted_scopes`. If an admin revoked a scope between the original code redemption and the refresh, the rotated at+jwt still carried the wider scope set until the family expired naturally; the audit named this medium-severity. Add a SELECT inside the rotation transaction that re-intersects `effective_scope` with the user's still-granted set (`revoked_at IS NULL`). When the intersection is empty the rotation fails with `invalid_grant`; when it narrowed, the at+jwt is reminted with the narrower set. Both changes are wholly internal to the OIDC vertical; no RP integration changes, no `.sqlx/` offline-cache regeneration (the new column lands behind runtime queries), no schema impact on `refresh_tokens_v2`. Reuse detection and family revocation paths untouched. #BUNYIP-262