fix(auth): bind userinfo.sub == claims.sub; truthful email_verified_at #404

Merged
YousifShkara merged 1 commit from fix/MAPPS-335-sub-binding-and-email-verified-truth into main 2026-06-30 06:33:21 +02:00
Owner

MAPPS-335: two related Bunyip-RS hardening items the 2026-06-30 audit named medium-severity.

Sub-binding: ensure_user_from_bunyip previously read info.email, info.email_verified, info.given_name, info.family_name directly off the /oauth2/userinfo response without asserting the response's sub matched the at+jwt's verified sub. A misbehaving / compromised OP (load-balancer bug, attacker-influenced response) injected ANOTHER user's identity into the JIT row keyed on claims.sub. The at+jwt signature was already validated; the userinfo response is only trusted to belong to the same subject. Filter the UserInfo to keep only the response when info.sub == claims.sub; on mismatch, log warn and treat as "no userinfo claims" so the JIT path falls back to the placeholder email + synthetic name. The request still succeeds (no 401 storm on a transient OP glitch), but no wrong-identity row is ever written.

Truthful email_verified_at: upsert_user_from_oidc stamped email_verified_at = NOW() unconditionally, including the placeholder sub@unresolved.invalid branch reached when the IdP reported email_verified=false. Downstream gates that read email_verified_at IS NOT NULL (invite consumption, certain admin paths) got a false positive. Plumb the actual email_verified flag through; the column lands as NULL when the IdP says false, NOW() only when true. The ON CONFLICT DO UPDATE clause was extended with a COALESCE on email_verified_at so an existing legitimate verification timestamp is preserved even when a later JIT call passes false.

No migration; the column already exists and the change is purely a write-side truthfulness fix.

#MAPPS-335

MAPPS-335: two related Bunyip-RS hardening items the 2026-06-30 audit named medium-severity. Sub-binding: `ensure_user_from_bunyip` previously read `info.email`, `info.email_verified`, `info.given_name`, `info.family_name` directly off the `/oauth2/userinfo` response without asserting the response's `sub` matched the at+jwt's verified `sub`. A misbehaving / compromised OP (load-balancer bug, attacker-influenced response) injected ANOTHER user's identity into the JIT row keyed on `claims.sub`. The at+jwt signature was already validated; the userinfo response is only trusted to belong to the same subject. Filter the `UserInfo` to keep only the response when `info.sub == claims.sub`; on mismatch, log warn and treat as "no userinfo claims" so the JIT path falls back to the placeholder email + synthetic name. The request still succeeds (no 401 storm on a transient OP glitch), but no wrong-identity row is ever written. Truthful email_verified_at: `upsert_user_from_oidc` stamped `email_verified_at = NOW()` unconditionally, including the placeholder `sub@unresolved.invalid` branch reached when the IdP reported `email_verified=false`. Downstream gates that read `email_verified_at IS NOT NULL` (invite consumption, certain admin paths) got a false positive. Plumb the actual `email_verified` flag through; the column lands as NULL when the IdP says false, NOW() only when true. The `ON CONFLICT DO UPDATE` clause was extended with a `COALESCE` on `email_verified_at` so an existing legitimate verification timestamp is preserved even when a later JIT call passes false. No migration; the column already exists and the change is purely a write-side truthfulness fix. #MAPPS-335
fix(auth): bind userinfo.sub == claims.sub; truthful email_verified_at
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m59s
Integration / integration tests (pull_request) Successful in 9m21s
E2E / Playwright against staging (pull_request) Successful in 1m24s
Create release / Gate (release-branch merges only) (pull_request) Successful in 1s
Create release / Create release from merged PR (pull_request) Has been skipped
9e70ecca79
MAPPS-335: two related Bunyip-RS hardening items the 2026-06-30 audit named medium-severity.

Sub-binding: `ensure_user_from_bunyip` previously read `info.email`, `info.email_verified`, `info.given_name`, `info.family_name` directly off the `/oauth2/userinfo` response without asserting the response's `sub` matched the at+jwt's verified `sub`. A misbehaving / compromised OP (load-balancer bug, attacker-influenced response) injected ANOTHER user's identity into the JIT row keyed on `claims.sub`. The at+jwt signature was already validated; the userinfo response is only trusted to belong to the same subject. Filter the `UserInfo` to keep only the response when `info.sub == claims.sub`; on mismatch, log warn and treat as "no userinfo claims" so the JIT path falls back to the placeholder email + synthetic name. The request still succeeds (no 401 storm on a transient OP glitch), but no wrong-identity row is ever written.

Truthful email_verified_at: `upsert_user_from_oidc` stamped `email_verified_at = NOW()` unconditionally, including the placeholder `sub@unresolved.invalid` branch reached when the IdP reported `email_verified=false`. Downstream gates that read `email_verified_at IS NOT NULL` (invite consumption, certain admin paths) got a false positive. Plumb the actual `email_verified` flag through; the column lands as NULL when the IdP says false, NOW() only when true. The `ON CONFLICT DO UPDATE` clause was extended with a `COALESCE` on `email_verified_at` so an existing legitimate verification timestamp is preserved even when a later JIT call passes false.

No migration; the column already exists and the change is purely a write-side truthfulness fix.

#MAPPS-335
YousifShkara deleted branch fix/MAPPS-335-sub-binding-and-email-verified-truth 2026-06-30 06:33:21 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!404
No description provided.