# 35. OIDC Token Refresh Mechanism in AuthorizationInterceptors Date: 2024-02-15 Updated: 2026-02-09 ## Status Accepted (supersedes [ADR-0031](0031-fix-refresh-token-with-oidc.md)) ## Context Multiple issues drove the evolution of the refresh token logic: 1. **Concurrent 401s caused logout** ([#1974](https://github.com/linagora/tmail-flutter/issues/1974)) — parallel requests with stale headers all triggered independent refresh attempts. 2. **Ugly error on expired token** ([#2592](https://github.com/linagora/tmail-flutter/issues/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.