Streamlining JWT Token Refresh in Flutter with Proactive Handling

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.

  1. Access token grants the user authentication and is short-lived (can range from 15 minutes to 2 hours)

  2. 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.

  1. Proactive approach checks if the access token expired and tries to refresh it before executing the request.

  2. 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 the refreshAccessToken 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!