Streamlining JWT Token Refresh in Flutter with Proactive Handling
Introduction
In the realm of app development, ensuring a secure and seamless user authentication flow is paramount. Utilizing JSON Web Tokens (JWT) for user sessions is a common practice that embodies security and efficiency.
JWT tokens usually come as a pair: access token and refresh token.
Access token grants the user authentication and is short-lived (can range from 15 minutes to 2 hours)
Refresh token is used to refresh the access token after it's expired and is usually long-lived (can vary from 2 weeks to 2 months or longer).
There are generally 2 ways to ensure that the refreshing is done seamlessly: proactive and reactive.
Proactive approach checks if the access token expired and tries to refresh it before executing the request.
Reactive approach executes the request first. Then, if the request fails with a status code 401 Unauthorised, the access token is refreshed and the request is executed again.
In this article, I'll show you how to use the proactive approach to JWT token refresh in Flutter and illustrate how to integrate it with the most popular libraries like dio
and graphql_flutter
.
Writing a custom http.BaseClient
Choosing to extend http.BaseClient
is necessary to check the access token validity and to refresh it before actually executing the HTTP request. It's also very convenient because you can use it for both REST and GraphQL clients since they both rely on HTTP protocol.
Below is a code snippet showing a custom HTTP client class in Flutter, designed to proactively handle JWT token refreshing:
import 'package:http/http.dart' as http;
import 'package:jwt_decoder/jwt_decoder.dart';
class AuthHttpClient extends http.BaseClient {
AuthHttpClient({
String? initialAccessToken,
required this.refreshAccessToken,
required this.onRefreshAccessTokenFailed,
}) : _accessToken = initialAccessToken ?? '';
String _accessToken;
final Future<String?> Function() refreshAccessToken;
final void Function() onRefreshAccessTokenFailed;
final http.Client _client = http.Client();
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
await _ensureValidAccessToken();
request.headers['Authorization'] = 'Bearer $_accessToken';
return _client.send(request);
}
@override
void close() {
_client.close();
super.close();
}
Future<String> fetchValidAccessToken() async {
await _ensureValidAccessToken();
return _accessToken;
}
Future<void> _ensureValidAccessToken() async {
if (_accessToken.isEmpty || JwtDecoder.isExpired(_accessToken)) {
final result = await refreshAccessToken();
if (result == null) {
onRefreshAccessTokenFailed();
} else {
_accessToken = result;
}
}
}
}
AuthHttpClient
is a class which takes in 3 parameters:
initialAccessToken
(optional): Initial access token. If it's not provided (null), the _accessToken is initialized to an empty string. This allows the AuthHttpClient to be instantiated without an initial token, and it will attempt to refresh the token when it's first needed.refreshAccessToken
: A function to refresh the access token when needed.onRefreshAccessTokenFailed
: A callback function that is invoked if therefreshAccessToken
function fails to retrieve a new token.
The send
method is overridden to ensure that a valid access token is available before attaching it to the Authorization header of outgoing HTTP requests.
The fetchValidAccessToken
provides a way to explicitly fetch a valid token. It can be used when you need a valid token, but you are unable to use AuthHttpClient
directly.
The _ensureValidAccessToken
is responsible for validating the current access token. If the token is empty or expired(jwt_decoder
package's isExpired
method is used here) the refreshAccessToken
function is invoked to attempt a token refresh. In case the token refresh fails (this is indicated by a null
return value), the onRefreshFailed
callback is triggered to handle the failure scenario. Otherwise, it updates the private _accessToken
with the new valid token.
The whole process can be visualized with a flow chart:
The most common way to handle refreshing access token failure is to log the user out (logging the user obtains the new tokens). The easiest way I've found to do this is to expose a Stream
or use a premade solution like eventbus
.
Dio Integration
Dio is a powerful library for making HTTP requests. To use AuthHttpClient
with Dio, you can create a custom Dio interceptor that utilizes AuthHttpClient
to handle the authorization header and token refreshing.
class AuthInterceptor extends Interceptor {
final AuthHttpClient authHttpClient;
AuthInterceptor(this.authHttpClient);
@override
onRequest(RequestOptions options) async {
final token = await authHttpClient.fetchValidAccessToken();
options.headers['Authorization'] = 'Bearer $token';
return options;
}
}
GraphQL Integration
For handling GraphQL queries, you can choose a library such as ferry
or graphql_flutter
. Create a custom HTTP link that employs AuthHttpClient
to manage the authorization header and token refreshing, ensuring secure GraphQL queries.
final exampleAccessToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2huRG9lIiwiaWF0IjoxNjMwMjI5MDIyLCJleHAiOjE2MzAyMzI2MjJ9.W27ZMQHnWt_6PxgVPz_1kMb6P_0VqGjI4Hq9Qd5zt8M'
final authHttpClient = AuthHttpClient(
initialAccessToken: exampleAccessToken,
refreshAccessToken: () async {
// Implement your token refresh logic here.
return 'your_new_access_token';
},
onRefreshAccessTokenFailed: () {
print('Token refresh failed');
},
);
final httpLink = HttpLink(
'https://your-graphql-endpoint.com/graphql',
httpClient: authHttpClient,
);
final client = GraphQLClient(
cache: GraphQLCache(),
link: httpLink,
);
Conclusion
Mastering the proactive JWT token refreshing approach in Flutter and integrating it with HTTP client libraries like Dio or Ferry makes the app's authentication flow more robust and testable.
This article provides the insights and code snippets to help you achieve a secure and user-friendly app. By handling token refreshes proactively, you ensure a smooth user experience, making sure your app remains secure and user-centric, even as the JWTs go through their natural lifecycle.
If you have enjoyed or found this helpful, follow me for more content like this!
Subscribe to my newsletter
Read articles from Dinko Marinac directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Dinko Marinac
Dinko Marinac
Mobile app developer and consultant. CEO @ MOBILAPP Solutions. Passionate about the Dart & Flutter ecosystem.