Files
workavia-mail-front/docs/adr/0035-error-handling-on-no-longer-valid-oidc-token.md
T
2026-02-10 16:58:58 +07:00

2.7 KiB

35. OIDC Token Refresh Mechanism in AuthorizationInterceptors

Date: 2024-02-15 Updated: 2026-02-09

Status

Accepted (supersedes ADR-0031)

Context

Multiple issues drove the evolution of the refresh token logic:

  1. Concurrent 401s caused logout (#1974) — parallel requests with stale headers all triggered independent refresh attempts.
  2. Ugly error on expired token (#2592) — users saw a red error instead of a silent redirect.
  3. 400 on refresh not handled — when the refresh token itself was revoked/expired, the server returned 400 (invalid_grant) but the interceptor did not distinguish this from transient errors, leading to confusing failures.
  4. Server-side revocation before local expiry — relying on local isExpired check caused the interceptor to skip refresh when the server had already revoked the token.

Decision

AuthorizationInterceptors extends QueuedInterceptorsWrapper (Dio). The onError handler processes errors one at a time with the following ordered checks:

  1. _refreshAttemptedKey guard — if the request already attempted a refresh/retry, skip everything and propagate the error. Prevents infinite loops.
  2. validateToRetryTheRequestWithNewToken (checked first) — only on 401 responses: if _token was already updated by a preceding queued request, retry immediately with the new token. No refresh call needed. Non-401 errors (500, 403, etc.) are never retried here — they are server errors unrelated to authentication.
  3. validateToRefreshToken — if status is 401, auth type is OIDC, and access/refresh tokens are present, attempt refresh. The local isExpired check was removed; we trust the server's 401.
    • Success (different token): update _token, persist to cache, mark _refreshAttemptedKey, retry.
    • Success (same token — duplicate): propagate original error, no retry.
    • DioException 400: call clear() (wipe auth state) and reject with RefreshTokenFailedException — triggers silent logout.
    • DioException other: propagate original error, preserve OIDC state.
  4. Outer catch: ServerError/TemporarilyUnavailable wrapped in DioException; other exceptions forwarded via err.copyWith.

Consequences

  • Queued requests after a successful refresh retry without redundant refresh calls.
  • 400 from the OIDC provider causes immediate session cleanup and silent redirect to login.
  • No infinite retry loops thanks to the _refreshAttemptedKey per-request flag.
  • Server-side token revocation is handled correctly regardless of local expiry time.