Automatic JWT Token Refresh in Dio: Interceptor and QueryInterceptor
Servers verify authorization via the Authorization header in the format <auth-scheme> <auth-parameters>. Middleware processes this header for all protected endpoints. If data is missing or invalid, it returns 401 Unauthorized or 403 Forbidden.
Main schemes:
- Basic: Base64-encoded login:password (
Authorization: Basic bG9naW46cGFzc3dvcmQ=). Low security due to easy decoding. - Bearer: JWT token (
Authorization: Bearer <Token>). Supports expiration, requires refresh.
JWT consists of Header (algorithm), Payload (user data, exp), Signature (server secret). Format: {Header}.{Payload}.{Signature} in Base64.
JWT Structure and Refresh
Payload contains exp (expiration time). On 401, clients request a refresh via a protected endpoint without middleware.
Dual-token systems:
- Access Token: Short-lived (minutes), for API requests.
- Refresh Token: Long-lived, for generating new Access tokens.
Strategies for Handling Expired Tokens
Logout (Simplest Approach)
On 401 — logout. Implementation is trivial but inconvenient for users.
Pros:
- Minimal code.
Cons:
- Frequent re-logins.
- Incompatible with dual-token.
Refresh on Error (Interceptor)
Implemented via Interceptor in Dio. Adds token in onRequest, handles 401 in onError.
class TokenInterceptor extends Interceptor {
final TokenStorage _storage;
TokenInterceptor(this._storage);
@override
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await _storage.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
}
Setup:
final dio = Dio()..interceptors.add(TokenInterceptor(TokenStorage()));
In onError for 401:
- Check
_isRefreshingflag. - If not — initiate refresh via a separate Dio.
- Queue requests wait via
Completer<void> _refreshCompleter.
Future<String?> _updateToken() async {
final oldToken = await _storage.getToken();
try {
final dio = Dio();
final response = await dio.get('/auth/refresh/', headers: {'Authorization': 'Bearer $oldToken'});
final newToken = (response.data as Map<String, dynamic>)['token'];
await _storage.setNewToken(newToken);
return newToken;
} catch (e) {
await _storage.clearToken();
return null;
}
}
Full onError logic synchronizes parallel requests, retries with new token.
Advantages:
- Works with dual-token.
- No external libraries.
Disadvantages:
- Wave load on server with mass retry.
- Response delay (error + refresh + retry).
Proactive Refresh (QueryInterceptor)
Uses exp from JWT to refresh before expiration. QueryInterceptor ensures sequential request processing.
Parse JWT to extract exp:
- Decode Base64 Payload.
- Parse JSON, get timestamp.
Advantage: no blocking 401 errors. Disadvantage: strict queue, parallel requests are blocked.
Key Takeaways
- Use
Interceptorfor reactive refresh withCompleterfor synchronization. QueryInterceptorsuits proactive refresh based onexp.- Store tokens in isolated storage, use a separate Dio for refresh.
- Handle race conditions with multiple 401s.
- Dual-token enhances security: short access + refresh.
— Editorial Team
No comments yet.