2.7 KiB
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:
- Concurrent 401s caused logout (#1974) — parallel requests with stale headers all triggered independent refresh attempts.
- Ugly error on expired token (#2592) — users saw a red error instead of a silent redirect.
- 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.
- Server-side revocation before local expiry — relying on local
isExpiredcheck 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:
_refreshAttemptedKeyguard — if the request already attempted a refresh/retry, skip everything and propagate the error. Prevents infinite loops.validateToRetryTheRequestWithNewToken(checked first) — only on 401 responses: if_tokenwas 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.validateToRefreshToken— if status is 401, auth type is OIDC, and access/refresh tokens are present, attempt refresh. The localisExpiredcheck 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.
DioException400: callclear()(wipe auth state) and reject withRefreshTokenFailedException— triggers silent logout.DioExceptionother: propagate original error, preserve OIDC state.
- Success (different token): update
- Outer catch:
ServerError/TemporarilyUnavailablewrapped inDioException; other exceptions forwarded viaerr.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
_refreshAttemptedKeyper-request flag. - Server-side token revocation is handled correctly regardless of local expiry time.