fix(oidc): thread real acr/amr through op_session creation + refresh family #288

Merged
YousifShkara merged 2 commits from fix/BUNYIP-257-acr-amr-threading into main 2026-06-30 06:58:27 +02:00
Owner

BUNYIP-257: every op_session row and every rotated at+jwt previously carried hardcoded acr = "urn:bunyip:loa:pwd" + amr = ["pwd"] regardless of the actual authentication method. A TOTP-completed login, a magic-link login, and a vanilla password login were indistinguishable to RPs. RPs that gate step-up auth on amr contains "mfa" / "otp" were misled; the OIDC at+jwt was reporting pwd for users who had cleared a second factor.

establish_op_session signature change. The bunyip-api wrapper now takes acr + amr from the caller and passes them through to OidcProvider::create_op_session (which already had the parameters). Three named constants live next to the function: ACR_PASSWORD, ACR_MFA, ACR_OTP. The five existing call sites in auth.rs pass the right values per flow:

  • register / login (no MFA) / accept_invite / setup_admin: ACR_PASSWORD + ["pwd"].
  • verify_magic_link: ACR_OTP + ["otp"].
  • totp::verify: ACR_MFA + ["pwd", "mfa"] (TOTP verify only fires after a password attempt that returned TwoFactorRequired).

Refresh family carries acr/amr forward. New migration 20260701000020_refresh_family_acr_amr.sql adds acr TEXT NOT NULL DEFAULT 'urn:bunyip:loa:pwd' and amr TEXT[] NOT NULL DEFAULT ARRAY['pwd'] to refresh_token_families. issue_refresh_token gains acr/amr parameters and persists them on the family. rotate_refresh_token JOINs the family so RotatedTokens carries them on every rotation. handle_refresh_grant reads from the rotation result instead of stamping the hardcoded defaults. The legacy default columns let existing families finish out their idle/absolute TTLs without a schema break.

Code-to-token exchange passes code_row.acr / code_row.amr (already on oauth_authorization_codes since BUNYIP-62) into issue_refresh_token, so the truthful values are persisted on the family from minute one.

Silent-SSO branches in oidc.rs:649 + :705 (the hub access_token / refresh_token cookie paths) STILL hardcode pwd/pwd because the hub HS256 JWT claims don't carry acr/amr. Fixing that requires expanding AccessTokenClaims / RefreshTokenClaims to ride those fields, which is its own follow-up ticket (the audit's silent-SSO concern is bigger than acr/amr alone).

#BUNYIP-257

BUNYIP-257: every op_session row and every rotated at+jwt previously carried hardcoded `acr = "urn:bunyip:loa:pwd"` + `amr = ["pwd"]` regardless of the actual authentication method. A TOTP-completed login, a magic-link login, and a vanilla password login were indistinguishable to RPs. RPs that gate step-up auth on `amr contains "mfa" / "otp"` were misled; the OIDC at+jwt was reporting `pwd` for users who had cleared a second factor. establish_op_session signature change. The bunyip-api wrapper now takes `acr` + `amr` from the caller and passes them through to `OidcProvider::create_op_session` (which already had the parameters). Three named constants live next to the function: `ACR_PASSWORD`, `ACR_MFA`, `ACR_OTP`. The five existing call sites in `auth.rs` pass the right values per flow: - `register` / `login` (no MFA) / `accept_invite` / `setup_admin`: ACR_PASSWORD + ["pwd"]. - `verify_magic_link`: ACR_OTP + ["otp"]. - `totp::verify`: ACR_MFA + ["pwd", "mfa"] (TOTP verify only fires after a password attempt that returned TwoFactorRequired). Refresh family carries acr/amr forward. New migration `20260701000020_refresh_family_acr_amr.sql` adds `acr TEXT NOT NULL DEFAULT 'urn:bunyip:loa:pwd'` and `amr TEXT[] NOT NULL DEFAULT ARRAY['pwd']` to `refresh_token_families`. `issue_refresh_token` gains `acr`/`amr` parameters and persists them on the family. `rotate_refresh_token` JOINs the family so `RotatedTokens` carries them on every rotation. `handle_refresh_grant` reads from the rotation result instead of stamping the hardcoded defaults. The legacy default columns let existing families finish out their idle/absolute TTLs without a schema break. Code-to-token exchange passes `code_row.acr` / `code_row.amr` (already on `oauth_authorization_codes` since BUNYIP-62) into `issue_refresh_token`, so the truthful values are persisted on the family from minute one. Silent-SSO branches in oidc.rs:649 + :705 (the hub `access_token` / `refresh_token` cookie paths) STILL hardcode pwd/pwd because the hub HS256 JWT claims don't carry acr/amr. Fixing that requires expanding `AccessTokenClaims` / `RefreshTokenClaims` to ride those fields, which is its own follow-up ticket (the audit's silent-SSO concern is bigger than acr/amr alone). #BUNYIP-257
fix(oidc): thread real acr/amr through op_session creation + refresh family
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 57s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
02907ba6a7
BUNYIP-257: every op_session row and every rotated at+jwt previously carried hardcoded `acr = "urn:bunyip:loa:pwd"` + `amr = ["pwd"]` regardless of the actual authentication method. A TOTP-completed login, a magic-link login, and a vanilla password login were indistinguishable to RPs. RPs that gate step-up auth on `amr contains "mfa" / "otp"` were misled; the OIDC at+jwt was reporting `pwd` for users who had cleared a second factor.

establish_op_session signature change. The bunyip-api wrapper now takes `acr` + `amr` from the caller and passes them through to `OidcProvider::create_op_session` (which already had the parameters). Three named constants live next to the function: `ACR_PASSWORD`, `ACR_MFA`, `ACR_OTP`. The five existing call sites in `auth.rs` pass the right values per flow:

- `register` / `login` (no MFA) / `accept_invite` / `setup_admin`: ACR_PASSWORD + ["pwd"].
- `verify_magic_link`: ACR_OTP + ["otp"].
- `totp::verify`: ACR_MFA + ["pwd", "mfa"] (TOTP verify only fires after a password attempt that returned TwoFactorRequired).

Refresh family carries acr/amr forward. New migration `20260701000020_refresh_family_acr_amr.sql` adds `acr TEXT NOT NULL DEFAULT 'urn:bunyip:loa:pwd'` and `amr TEXT[] NOT NULL DEFAULT ARRAY['pwd']` to `refresh_token_families`. `issue_refresh_token` gains `acr`/`amr` parameters and persists them on the family. `rotate_refresh_token` JOINs the family so `RotatedTokens` carries them on every rotation. `handle_refresh_grant` reads from the rotation result instead of stamping the hardcoded defaults. The legacy default columns let existing families finish out their idle/absolute TTLs without a schema break.

Code-to-token exchange passes `code_row.acr` / `code_row.amr` (already on `oauth_authorization_codes` since BUNYIP-62) into `issue_refresh_token`, so the truthful values are persisted on the family from minute one.

Silent-SSO branches in oidc.rs:649 + :705 (the hub `access_token` / `refresh_token` cookie paths) STILL hardcode pwd/pwd because the hub HS256 JWT claims don't carry acr/amr. Fixing that requires expanding `AccessTokenClaims` / `RefreshTokenClaims` to ride those fields, which is its own follow-up ticket (the audit's silent-SSO concern is bigger than acr/amr alone).

#BUNYIP-257
Merge remote-tracking branch 'origin/main' into fix/BUNYIP-257-acr-amr-threading
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 40s
Check / fmt + clippy + build + tests (pull_request) Successful in 9m44s
Create release / Create release from merged PR (pull_request) Has been skipped
50978e6371
# Conflicts:
#	bunyip-api/src/handlers/auth.rs
#	bunyip-api/src/handlers/totp.rs
YousifShkara deleted branch fix/BUNYIP-257-acr-amr-threading 2026-06-30 06:58:28 +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/bunyip!288
No description provided.