
Flutter: Handling JWT Token Refresh with Dio Interceptors
Stop forcing users to login every time. Learn how to implement seamless JWT Token Refresh using Dio Interceptors, request queuing, and silent retry logic.

Stop forcing users to login every time. Learn how to implement seamless JWT Token Refresh using Dio Interceptors, request queuing, and silent retry logic.
A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Establishing TCP connection is expensive. Reuse it for multiple requests.

Yellow stripes appear when the keyboard pops up? Learn how to handle layout overflows using resizeToAvoidBottomInset, SingleChildScrollView, and tricks for chat apps.

HTTP is Walkie-Talkie (Over). WebSocket is Phone (Hello). The secret tech behind Chat and Stock Charts.

The fastest way to lose users is forcing them to log in every 30 minutes. Security demands short-lived Access Tokens, but UX demands persistent sessions.
Our goal is "Silent Token Refresh": Updating the token behind the scenes without the user noticing.
The Flow:
Using dio package, we can intercept errors globally.
Use QueuedInterceptorsWrapper to handle concurrent requests automatically.
dio.interceptors.add(
QueuedInterceptorsWrapper(
onError: (DioException err, handler) async {
// 1. Check if error is 401 Unauthorized
if (err.response?.statusCode == 401) {
// 2. Prevent infinite loop
// (if refresh endpoint itself returns 401, we are doomed)
if (err.requestOptions.path.contains('/refresh')) {
return handler.next(err);
}
try {
// 3. Get Refresh Token from Secure Storage
final refreshToken = await storage.read(key: 'refreshToken');
// 4. Request New Token (Use a separate Dio instance to avoid loop)
final response = await Dio().post(
'https://api.com/refresh',
data: {'token': refreshToken}
);
final newAccessToken = response.data['accessToken'];
await storage.write(key: 'accessToken', value: newAccessToken);
// 5. Clone the failed request and update header
final options = err.requestOptions;
options.headers['Authorization'] = 'Bearer $newAccessToken';
// 6. Retry
final retryResponse = await dio.fetch(options);
// 7. Resolve the original request with new response
return handler.resolve(retryResponse);
} catch (e) {
// If refresh fails, force logout
await _forceLogout();
return handler.next(err);
}
}
return handler.next(err);
},
),
);
Imagine the homepage fires 5 API calls simultaneously.
All 5 fail with 401.
Without queueing, your app might send 5 Refresh Requests.
The server might invalidate the refresh token after the first use, causing the other 4 to fail with Invalid Token.
QueuedInterceptorsWrapper handles this. When the first 401 is hit, it Locks the interceptor chain.
Subsequent requests are paused (Queued) until handler.resolve() is called.
Once the token is refreshed, the queue is released, and all pending requests are retried with the new token.
For banking-level security, use Rotation. Every time you use a Refresh Token, the server issues a new Refresh Token and invalidates the old one.
If a hacker steals your Refresh Token:
A: Check if your /refresh endpoint itself requires an Access Token. It should be public or require only a Refresh Token. Also, ensure you are NOT using the intercepted Dio instance inside the interceptor.
A: As mentioned, use QueuedInterceptorsWrapper. It automatically queues simultaneous requests when the lock is active.
A: Then the user must log out. There is no other way. Redirect them to the Login screen and clear secure storage.
Implementing the interceptor is only half the battle. Verify these checkpoints:
flutter_secure_storage with encryptedSharedPreferences enabled on Android? Plain SharedPreferences is plain text.print(token))? Release builds should utilize release mode log stripping or a logger that sanitizes secrets.admin privileges to a user token.A compact, URL-safe means of representing claims to be transferred between two parties. It consists of three parts: Header, Payload, and Signature. It is stateless, meaning the server doesn't need to store session data.
The predominant type of Access Token used with OAuth 2.0. "Bearer" means "Give access to the bearer of this token." If you lose it, anyone who finds it can use it (like cash). This is why keeping it short-lived is crucial.
A vulnerability where an attacker injects malicious scripts into a trusted website. If you store tokens in localStorage or sessionStorage, XSS attacks can easily steal them. Ideally, tokens should be stored in HttpOnly cookies or memory (with Refresh Tokens in Secure Storage).
An attack that forces an end user to execute unwanted actions on a web application in which they are currently authenticated. HttpOnly cookies are vulnerable to CSRF, but SameSite=Strict attribute helps mitigate this.
A security measure where a new Refresh Token is issued every time the old one is used. This detects token theft: if an old refresh token is reused, the server knows it's been stolen and invalidates the entire token family.
The process of refreshing an Access Token without user interaction. In web apps, this is often done via a hidden iframe using cookies. In mobile apps, it's done via background API calls using the stored Refresh Token.
When a token expires and refresh fails (e.g., user changed password on another device), the "Force Logout" experience is critical.
secure_storage and local database to prevent data leaks./refresh endpoint transparently.dio.fetch.flutter_secure_storage.A seamless auth experience is invisible. It just works.