38 lines
2.7 KiB
Markdown
38 lines
2.7 KiB
Markdown
# 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.
|