All found by the multi-angle /code-review over the branch and verified
against the installed libraries:
- 401 discriminator: the session-expiry redirect keyed off a path prefix
(!/auth/*) and falsely logged users out on /profile/change-password
with a wrong current password. requireAuth's no-session 401 now carries
the distinct AUTH_REQUIRED code — the only signal api.ts dispatches
auth:expired on; wrong-credential 401s keep AUTH_ERROR on any path.
Test updated to pin the code-based contract on both directions.
- OIDC RFC 9207: exchangeAndVerifyTokens rebuilt the callback URL with
only code+state, dropping the iss parameter openid-client v6 validates
when the provider advertises support (authentik does) — every OIDC
login would fail post-upgrade. The full callback query is now forwarded.
- OIDC http issuers: v6 enforces HTTPS on discovery/endpoints (v5
didn't) — breaking internal http:// providers on Docker networks.
OIDC_ALLOW_INSECURE_HTTP=true opts back in (documented in .env.example).
- 2FA downgrade: authService's webauthn fallback caught ANY challenge
error and silently downgraded to password-only login; now only the
stale-flag 'No registered WebAuthn credentials' case falls through,
everything else rethrows.
- Burned-challenge retry: the server consumes the single-use 2FA
challenge before verifying (anti-replay), so after a server rejection
a retry can only ever return 'Challenge expired'. LoginPage now resets
to sign-in on server failure (WebAuthn AND the pre-existing TOTP trap);
a browser-prompt cancel keeps retry (token still live). Also lazy-loads
@simplewebauthn/browser out of the entry bundle, matching ProfilePage.
Server 252/252, client 52/52, OIDC smoke 44/44, probe 33/33, e2e 27.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
useAuth only checked /auth/me on mount, so a session that expired later left
the app half-alive: mutations failed with raw 401 toasts and no way back to
login. api.ts now dispatches 'auth:expired' on any 401 outside /auth/*
(under /auth/* a 401 means wrong credential input — e.g. change-password —
and must not log the user out); AuthProvider listens and clears the user, so
the existing RequireAuth redirect kicks in preserving state.from.
Test: client/api.sessionExpiry.test.ts (both directions).
(QA follow-up 1e, error-handling audit)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>