fix(auth): kid-aware JWKS refresh + cap force-refresh cadence + active checks on legacy fallback #405

Merged
YousifShkara merged 1 commit from fix/MAPPS-337-jwks-kid-aware-redirect-cap-active-checks into main 2026-06-30 06:01:35 +02:00
Owner

MAPPS-337: three related Bunyip-RS hardening items from the 2026-06-30 audit.

JWKS amplification defense: verify_at_jwt previously force-refreshed JWKS on every InvalidSignature, conflating "wrong key, signature does not match the cached JWK" with "unknown kid, the OP may have rotated". A junk-token spray (sign random tokens with the issuer's expected header but random data) would trigger one JWKS fetch per attempt because every signature would fail. Introduce a new VerifyError::UnknownKid variant returned only when header.kid is absent from the cached set; the retry-on-refresh path triggers ONLY on that. Genuine InvalidSignature failures fall closed without any extra network IO.

Force-refresh cooldown: even legitimate UnknownKid paths should not fan out to the OP at arbitrary rates. Add a per-process cooldown (FORCE_REFRESH_COOLDOWN_SECS = 60) tracked through last_force_refresh. Sequential UnknownKid attempts inside the window short-circuit to Ok(()) so the caller's next try_validate still returns UnknownKid (Unauthorized) without a network call. The existing refresh_lock coalesces concurrent calls; the new cooldown defends sequentially.

JWKS HTTP client refuses redirects: the JWKS / discovery client built reqwest::Client with the default redirect policy (up to 10 follows). A misconfigured DNS or a 302 from the issuer would chase the chain into attacker-chosen territory. .redirect(reqwest::redirect::Policy::none()) so the legitimate OP must serve the doc directly or the fetch fails.

Legacy fallback active checks: the legacy HS256 cookie path authenticated a user as long as the JWT decoded and the row existed; users.status != Active and a suspended tenant were ignored, so revocations only took effect at token TTL. Add a public AuthService::ensure_user_and_tenant_active (mirroring the checks login() runs) and call it from the middleware before populating AuthState. Revocations take effect on the very next request.

#MAPPS-337

MAPPS-337: three related Bunyip-RS hardening items from the 2026-06-30 audit. JWKS amplification defense: `verify_at_jwt` previously force-refreshed JWKS on every `InvalidSignature`, conflating "wrong key, signature does not match the cached JWK" with "unknown kid, the OP may have rotated". A junk-token spray (sign random tokens with the issuer's expected header but random data) would trigger one JWKS fetch per attempt because every signature would fail. Introduce a new `VerifyError::UnknownKid` variant returned only when `header.kid` is absent from the cached set; the retry-on-refresh path triggers ONLY on that. Genuine `InvalidSignature` failures fall closed without any extra network IO. Force-refresh cooldown: even legitimate `UnknownKid` paths should not fan out to the OP at arbitrary rates. Add a per-process cooldown (`FORCE_REFRESH_COOLDOWN_SECS = 60`) tracked through `last_force_refresh`. Sequential UnknownKid attempts inside the window short-circuit to `Ok(())` so the caller's next `try_validate` still returns `UnknownKid` (Unauthorized) without a network call. The existing `refresh_lock` coalesces concurrent calls; the new cooldown defends sequentially. JWKS HTTP client refuses redirects: the JWKS / discovery client built `reqwest::Client` with the default redirect policy (up to 10 follows). A misconfigured DNS or a 302 from the issuer would chase the chain into attacker-chosen territory. `.redirect(reqwest::redirect::Policy::none())` so the legitimate OP must serve the doc directly or the fetch fails. Legacy fallback active checks: the legacy HS256 cookie path authenticated a user as long as the JWT decoded and the row existed; `users.status != Active` and a suspended tenant were ignored, so revocations only took effect at token TTL. Add a public `AuthService::ensure_user_and_tenant_active` (mirroring the checks `login()` runs) and call it from the middleware before populating `AuthState`. Revocations take effect on the very next request. #MAPPS-337
fix(auth): kid-aware JWKS refresh + cap force-refresh cadence + active checks on legacy fallback
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 1m24s
Check / fmt + clippy + build + tests (pull_request) Successful in 5m8s
Integration / integration tests (pull_request) Successful in 15m20s
Create release / Gate (release-branch merges only) (pull_request) Successful in 1s
Create release / Create release from merged PR (pull_request) Has been skipped
5722869f7c
MAPPS-337: three related Bunyip-RS hardening items from the 2026-06-30 audit.

JWKS amplification defense: `verify_at_jwt` previously force-refreshed JWKS on every `InvalidSignature`, conflating "wrong key, signature does not match the cached JWK" with "unknown kid, the OP may have rotated". A junk-token spray (sign random tokens with the issuer's expected header but random data) would trigger one JWKS fetch per attempt because every signature would fail. Introduce a new `VerifyError::UnknownKid` variant returned only when `header.kid` is absent from the cached set; the retry-on-refresh path triggers ONLY on that. Genuine `InvalidSignature` failures fall closed without any extra network IO.

Force-refresh cooldown: even legitimate `UnknownKid` paths should not fan out to the OP at arbitrary rates. Add a per-process cooldown (`FORCE_REFRESH_COOLDOWN_SECS = 60`) tracked through `last_force_refresh`. Sequential UnknownKid attempts inside the window short-circuit to `Ok(())` so the caller's next `try_validate` still returns `UnknownKid` (Unauthorized) without a network call. The existing `refresh_lock` coalesces concurrent calls; the new cooldown defends sequentially.

JWKS HTTP client refuses redirects: the JWKS / discovery client built `reqwest::Client` with the default redirect policy (up to 10 follows). A misconfigured DNS or a 302 from the issuer would chase the chain into attacker-chosen territory. `.redirect(reqwest::redirect::Policy::none())` so the legitimate OP must serve the doc directly or the fetch fails.

Legacy fallback active checks: the legacy HS256 cookie path authenticated a user as long as the JWT decoded and the row existed; `users.status != Active` and a suspended tenant were ignored, so revocations only took effect at token TTL. Add a public `AuthService::ensure_user_and_tenant_active` (mirroring the checks `login()` runs) and call it from the middleware before populating `AuthState`. Revocations take effect on the very next request.

#MAPPS-337
YousifShkara deleted branch fix/MAPPS-337-jwks-kid-aware-redirect-cap-active-checks 2026-06-30 06:01:35 +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!405
No description provided.