fix(oidc): silent SSO also consumes refresh_token cookie on /authorize #90

Merged
YousifShkara merged 1 commit from fix/oidc-silent-sso-via-access-token into main 2026-06-10 02:33:17 +02:00
Owner

Extends the access_token-based silent SSO with a refresh_token fallback so the closing edge case from the previous commit no longer requires a credentialed re-login. Concretely: after the browser auto-expires the 15-min access_token cookie (Max-Age elapsed; the JWT itself may also have expired), but the 30-day refresh_token cookie is still in the jar, /oauth2/authorize now runs the same DB-tracked rotation that /v1/auth/refresh does, sets the rotated access + refresh cookies alongside the new bunyip_op_session cookie, and continues into the code-mint path. The browser leaves the request in the same state it would be in after a manual /v1/auth/refresh round-trip plus a successful authorize, just folded into a single request.

DMARC-21 closes completely: a user who comes back to drillmark days later, with only the refresh_token cookie still alive (op_session row expired, access_token cookie auto-deleted, sessionStorage bundle on the SPA side ALSO expired) walks through start_login -> /authorize once and lands on /dashboard with no login screen.

The helper now walks two paths in order:

  1. access_token cookie verifies via JwtService::verify_access_token -> mint op_session, return one Set-Cookie. Cheap, no DB writes besides the op_sessions insert. Catches "user is actively in bunyip-web."
  2. access_token absent or expired -> verify refresh_token JWT locally (so a forged token short-circuits with no DB write), then call AuthService::refresh_tokens for the full rotation (family-reuse detection + audit logging + token revocation untouched). Mint op_session, emit clear_stale + new access + new refresh + new op_session cookies. Catches the DMARC-21 case.

Falls through to the existing /login redirect when both inputs are gone, the refresh JWT is forged, or the rotation is rejected (revoked, family-reuse detected). Logout's existing posture (AuthCookies::clear removes all three cookies + revoke_op_sessions runs synchronously + auth_service.logout marks the refresh row revoked) leaves a logged-out user with no cookie input the silent path can consume.

The handler grows two web::Data params (JwtService was already added by the previous commit; AuthService is new) and routes extract_client_ip / extract_device_info from bunyip_domain::middleware::auth so the silent rotation looks identical to /v1/auth/refresh in audit logs.

#DMARC-21

Extends the access_token-based silent SSO with a refresh_token fallback so the closing edge case from the previous commit no longer requires a credentialed re-login. Concretely: after the browser auto-expires the 15-min `access_token` cookie (Max-Age elapsed; the JWT itself may also have expired), but the 30-day `refresh_token` cookie is still in the jar, `/oauth2/authorize` now runs the same DB-tracked rotation that `/v1/auth/refresh` does, sets the rotated access + refresh cookies alongside the new bunyip_op_session cookie, and continues into the code-mint path. The browser leaves the request in the same state it would be in after a manual `/v1/auth/refresh` round-trip plus a successful authorize, just folded into a single request. DMARC-21 closes completely: a user who comes back to drillmark days later, with only the refresh_token cookie still alive (op_session row expired, access_token cookie auto-deleted, sessionStorage bundle on the SPA side ALSO expired) walks through `start_login -> /authorize` once and lands on `/dashboard` with no login screen. The helper now walks two paths in order: 1. `access_token` cookie verifies via `JwtService::verify_access_token` -> mint op_session, return one Set-Cookie. Cheap, no DB writes besides the op_sessions insert. Catches "user is actively in bunyip-web." 2. `access_token` absent or expired -> verify `refresh_token` JWT locally (so a forged token short-circuits with no DB write), then call `AuthService::refresh_tokens` for the full rotation (family-reuse detection + audit logging + token revocation untouched). Mint op_session, emit clear_stale + new access + new refresh + new op_session cookies. Catches the DMARC-21 case. Falls through to the existing `/login` redirect when both inputs are gone, the refresh JWT is forged, or the rotation is rejected (revoked, family-reuse detected). Logout's existing posture (AuthCookies::clear removes all three cookies + revoke_op_sessions runs synchronously + auth_service.logout marks the refresh row revoked) leaves a logged-out user with no cookie input the silent path can consume. The handler grows two web::Data params (JwtService was already added by the previous commit; AuthService is new) and routes `extract_client_ip` / `extract_device_info` from `bunyip_domain::middleware::auth` so the silent rotation looks identical to `/v1/auth/refresh` in audit logs. #DMARC-21
fix(oidc): silent SSO also consumes refresh_token cookie on /authorize
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 1m57s
6b29275196
Extends the access_token-based silent SSO with a refresh_token fallback so the closing edge case from the previous commit no longer requires a credentialed re-login. Concretely: after the browser auto-expires the 15-min `access_token` cookie (Max-Age elapsed; the JWT itself may also have expired), but the 30-day `refresh_token` cookie is still in the jar, `/oauth2/authorize` now runs the same DB-tracked rotation that `/v1/auth/refresh` does, sets the rotated access + refresh cookies alongside the new bunyip_op_session cookie, and continues into the code-mint path. The browser leaves the request in the same state it would be in after a manual `/v1/auth/refresh` round-trip plus a successful authorize, just folded into a single request.

DMARC-21 closes completely: a user who comes back to drillmark days later, with only the refresh_token cookie still alive (op_session row expired, access_token cookie auto-deleted, sessionStorage bundle on the SPA side ALSO expired) walks through `start_login -> /authorize` once and lands on `/dashboard` with no login screen.

The helper now walks two paths in order:

  1. `access_token` cookie verifies via `JwtService::verify_access_token` -> mint op_session, return one Set-Cookie. Cheap, no DB writes besides the op_sessions insert. Catches "user is actively in bunyip-web."
  2. `access_token` absent or expired -> verify `refresh_token` JWT locally (so a forged token short-circuits with no DB write), then call `AuthService::refresh_tokens` for the full rotation (family-reuse detection + audit logging + token revocation untouched). Mint op_session, emit clear_stale + new access + new refresh + new op_session cookies. Catches the DMARC-21 case.

Falls through to the existing `/login` redirect when both inputs are gone, the refresh JWT is forged, or the rotation is rejected (revoked, family-reuse detected). Logout's existing posture (AuthCookies::clear removes all three cookies + revoke_op_sessions runs synchronously + auth_service.logout marks the refresh row revoked) leaves a logged-out user with no cookie input the silent path can consume.

The handler grows two web::Data params (JwtService was already added by the previous commit; AuthService is new) and routes `extract_client_ip` / `extract_device_info` from `bunyip_domain::middleware::auth` so the silent rotation looks identical to `/v1/auth/refresh` in audit logs.

#DMARC-21
YousifShkara deleted branch fix/oidc-silent-sso-via-access-token 2026-06-10 02:33:17 +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!90
No description provided.