logomenu

Refresh JWT Token Interceptor in Flutter

This tutorial will help you build an API client in Flutter that allows you to automatically refresh your access tokens in Flutter using dio.

Last Updated: December 26, 2021

flutter

jwt

This tutorial will help you build an API client in Flutter that allows you to automatically refresh your access tokens in Flutter. This tutorial will use Dio as our http client to make the requests.

To get started, this tutorial assumes that you are using REST api, that when logged in gets a refresh token and an access token. When the server responds with a 401 request you will refresh your token.

Setup

To get started create your flutter project by running flutter create flutter_refresh_token_tutorial in your terminal. With that finshed create a new directory inside of lib called core and inside of that add another directory called services. Then add api.dart to the services directory.

Http Flow

The purpose of this http client is so that every request will add the access token to the authorization header. Then on every response it checks if it is a 401 error, and if so it will refresh the access token and retry the request.

Create Client

To begin, we need dio to be installed so run flutter pub add dio in your terminal. With dio installed, we can begin coding.

In order for your http client to be accessible to the rest of your app you need to use some sort of state management solution and in this tutorial I use riverpod so our Api will just be a plain class. Your Api class will need three variables:

final Dio api = Dio(); String? accessToken; // will throw an error final _storage = const FlutterSecureStorage();

In order to store our refresh token securely in the app, install Flutter Secure Storage by running in the terminal: flutter pub add flutter_secure_storage.

Inside the constructor of Api is where we can add the access token to every request using interceptors with dio.

Api() { api.interceptors .add(InterceptorsWrapper(onRequest: (options, handler) async { if (!options.path.contains('http')) { options.path = 'your-server' + options.path; } options.headers['Authorization'] = 'Bearer $accessToken'; return handler.next(options); }, onError: (DioError error, handler) async { // todo: will finish this return handler.next(error); }));

You can see inside the constructor we are also checking if the request contains http and if not we are setting the path to the path to your server so you don't have to repeatedly add that to every request. Then we are setting the Authorization header to our accessToken. Then to run the rest of the request we return handler.next with our new options.

With that out of the way now we need to refresh the token if we get a 401 error returned. To do this we must check if the error status code is 401, and our error says: "Invalid JWT". The error message can depend on your server, but for this tutorial we will be using those values. Then we will need to check if we have a refresh token stored, and if so we must refresh it. This code is below:

if ((error.response?.statusCode == 401 && error.response?.data['message'] == "Invalid JWT")) { if (await _storage.containsKey(key: 'refreshToken')) { // will throw error below await refreshToken(); } }

To refresh our token we must create a new function called refreshToken which will refresh our token. This function may look different depending on your implementation of the refresh endpoint, but for us it will be like this:

Future<void> refreshToken() async { final refreshToken = await _storage.read(key: 'refreshToken'); final response = await api .post('/auth/refresh', data: {'refreshToken': refreshToken}); if (response.statusCode == 201) { // successfully got the new access token accessToken = response.data; } else { // refresh token is wrong so log out user. accessToken = null; _storage.deleteAll(); } }

With that function in place, our access token is set so now we can retry the request. Unfortunately, Dio doesn't provide an easy way to retry a request so we must create our own retry method that looks like the following:

Future<Response<dynamic>> _retry(RequestOptions requestOptions) async { final options = Options( method: requestOptions.method, headers: requestOptions.headers, ); return api.request<dynamic>(requestOptions.path, data: requestOptions.data, queryParameters: requestOptions.queryParameters, options: options); }

Now back in our constructor we can call this retry method like the following:

Api() { ... if (await _storage.containsKey(key: 'refreshToken')) { await refreshToken(); return handler.resolve(await _retry(error.requestOptions)); } } return handler.next(error); })); }

Make sure at the end of all the if statements to return handler.next(error) if it is just a general error.

Additional Steps

  1. If you wanted to make this function even safer, you could return a Future<bool> from refresh token and check if it returned true on the case of successful refresh token and return a false on unsuccessful so our interceptor can end the request if it could not refresh the token.

  2. Wrap this class inside of a provider, like in the case of riverpod, so you can use it in anywhere in the app.

Conclusion

Now we have a fully functioning refresh token in Flutter. Thank you for reading this article and if you enjoyed it share it on social media, or leave a comment below. As always the code is on Github (link is at the top of article).

Comments

Loading...