How to Handle JWT Authentication and Refresh Tokens in Flutter

How to Handle JWT Authentication and Refresh Tokens in Flutter

Managing user sessions securely is a critical requirement for modern mobile applications. When building an app with Flutter, handling JSON Web Tokens (JWT) correctly ensures that users stay authenticated without compromising security. A robust implementation requires attaching access tokens to protected requests and using a secure mechanism to refresh expired tokens seamlessly.

This guide walks you through setting up secure token storage, attaching access tokens via HTTP interceptors, and handling token refreshes automatically in Flutter.

Quick Overview

ComponentDetails
FrameworkFlutter
HTTP ClientDio Package
Secure StorageFlutter Secure Storage
Target FlowJWT Access & Refresh Token Rotation

Prerequisites

  • Flutter SDK (Version 3.0 or higher)
  • Basic understanding of asynchronous programming in Dart (async/await)
  • An active backend API supporting JWT authentication and refresh endpoints

Step 1: Installing Required Dependencies

Open your pubspec.yaml file and add the required packages for HTTP requests and encrypted local storage:

YAML

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.0
  flutter_secure_storage: ^9.0.0

Run flutter pub get in your terminal to fetch the packages. flutter_secure_storage ensures tokens are stored securely using Keychain on iOS and EncryptedSharedPreferences on Android.

Step 2: Creating a Secure Token Storage Service

Create a helper class to handle saving, reading, and deleting tokens safely from device storage.

Dart

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenStorage {
  static const _secureStorage = FlutterSecureStorage();
  
  static const _accessTokenKey = 'ACCESS_TOKEN';
  static const _refreshTokenKey = 'REFRESH_TOKEN';

  static Future<void> saveTokens({
    required String accessToken, 
    required String refreshToken,
  }) async {
    await _secureStorage.write(key: _accessTokenKey, value: accessToken);
    await _secureStorage.write(key: _refreshTokenKey, value: refreshToken);
  }

  static Future<String?> getAccessToken() async {
    return await _secureStorage.read(key: _accessTokenKey);
  }

  static Future<String?> getRefreshToken() async {
    return await _secureStorage.read(key: _refreshTokenKey);
  }

  static Future<void> clearTokens() async {
    await _secureStorage.delete(key: _accessTokenKey);
    await _secureStorage.delete(key: _refreshTokenKey);
  }
}

Step 3: Implementing Dio Interceptor for Automatic Token Refresh

The Dio package allows you to intercept outgoing requests and incoming errors. Using QueuedInterceptorsWrapper prevents race conditions by queuing incoming HTTP requests while a token refresh is in progress.

If an API call returns a 401 Unauthorized status code, the interceptor will automatically request a new access token using the refresh token, retry the failed request, or clear storage if the refresh token has expired.

Dart

import 'package:dio/dio.dart';
import 'token_storage.dart';

class DioClient {
  static final Dio dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.yourdomain.com/v1/',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
    ),
  );

  static void setupInterceptors() {
    dio.interceptors.add(
      QueuedInterceptorsWrapper(
        onRequest: (options, handler) async {
          // Attach Access Token to header if available
          final accessToken = await TokenStorage.getAccessToken();
          if (accessToken != null) {
            options.headers['Authorization'] = 'Bearer $accessToken';
          }
          return handler.next(options);
        },
        onError: (DioException error, handler) async {
          // Check if error is due to an expired access token (401 Unauthorized)
          if (error.response?.statusCode == 401) {
            final refreshToken = await TokenStorage.getRefreshToken();

            if (refreshToken != null) {
              try {
                // Request a new access token using an isolated Dio instance
                final refreshResponse = await Dio().post(
                  'https://api.yourdomain.com/v1/auth/refresh',
                  data: {'refreshToken': refreshToken},
                );

                final newAccessToken = refreshResponse.data['accessToken'];
                final newRefreshToken = refreshResponse.data['refreshToken'];

                // Save new tokens to encrypted storage
                await TokenStorage.saveTokens(
                  accessToken: newAccessToken,
                  refreshToken: newRefreshToken,
                );

                // Update original request headers with new access token
                final options = error.requestOptions;
                options.headers['Authorization'] = 'Bearer $newAccessToken';

                // Retry original request
                final clonedResponse = await dio.fetch(options);
                return handler.resolve(clonedResponse);
              } catch (refreshError) {
                // Refresh token is expired or invalid: clear session
                await TokenStorage.clearTokens();
                return handler.reject(error);
              }
            }
          }
          return handler.next(error);
        },
      ),
    );
  }
}

Step 4: Initializing DioClient in Your App

To activate the interceptor when your application starts, call setupInterceptors() inside your main() function before launching the UI:

Dart

import 'package:flutter/material.dart';
import 'dio_client.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize Dio interceptors
  DioClient.setupInterceptors();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(child: Text('Flutter JWT Auth Setup Complete')),
      ),
    );
  }
}

Summary

By combining flutter_secure_storage with Dio’s QueuedInterceptorsWrapper, your Flutter application can seamlessly maintain user sessions, handle token rotation securely, and execute background request retries without interrupting the user experience.

Leave a Reply

Your email address will not be published. Required fields are marked *

Solverwp- WordPress Theme and Plugin