Update ADR for OIDC token refresh

This commit is contained in:
Dat H. Pham
2026-02-09 14:01:12 +07:00
committed by Dat H. Pham
parent 8fe935ac30
commit b14bb6acc0
5 changed files with 90 additions and 26 deletions
+2 -2
View File
@@ -4,9 +4,9 @@ Date: 2023-09-11
## Status
- Issue:
Superseded by [ADR-0035](0035-error-handling-on-no-longer-valid-oidc-token.md)
[1974](https://github.com/linagora/tmail-flutter/issues/1974)
- Issue: [1974](https://github.com/linagora/tmail-flutter/issues/1974)
## Context
@@ -1,19 +1,37 @@
# 35. Error handling on no longer valid OIDC token (Issue #2592)
# 35. OIDC Token Refresh Mechanism in AuthorizationInterceptors
Date: 2024-02-15
Updated: 2026-02-09
## Status
Accepted
Accepted (supersedes [ADR-0031](0031-fix-refresh-token-with-oidc.md))
## Context
- When my OIDC token is expired, I see an awful red error message while being redirected
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
- When my OIDC token is expired, I am just redirected to the OIDC provider portal. No need to show an awful error.
`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
- In case of receiving a `BadCredentials` error, the system automatically logs out and returns to the login screen without any notification to the user.
- 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.