2

This is the code of AuthInterceptor which I use for triggering token refreshing. For safety (that I the interceptor of main dio instance won't attach accesstoken to refresh token api) I even use a different instance of dio for refresh token api call. I don't see what's wrong and why sometimes it causes 401 "Invalid Refresh Token" error.

class AuthInterceptor extends QueuedInterceptor {
final Dio _dio;
final FlutterSecureStorage _secureStorage;
bool _isRefreshing = false;
final List<RequestOptions> _pendingRequests = [];
final Dio _tokenDio = Dio(
    BaseOptions(
        baseUrl: baseURL,
        headers: {
            'accept': 'application/json',
            'Content-Type': 'application/json',
        },
    ),
)..interceptors.add(
    LogInterceptor(
        request: true,
        requestHeader: true,
        requestBody: true,
        responseBody: true,
    ),
);

AuthInterceptor(this._dio, this._secureStorage);

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final accessToken = await _secureStorage.read(key: 'accessToken');
    final refreshToken = await _secureStorage.read(key: 'refreshToken');

    // Ensure we're using accessToken, not refreshToken
    if (accessToken != null) {
        options.headers['Authorization'] = 'Bearer $accessToken';
        print('✅ Authorization header set with accessToken');
    } else {
        print('❌ Cannot set Authorization header - accessToken is null');
    }

    super.onRequest(options, handler);
}

@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
        // Token expired, attempt refresh
        if (_isRefreshing) {
            // If already refreshing, queue this request
            _pendingRequests.add(err.requestOptions);
            return;
        }

        final refreshToken = await _secureStorage.read(key: 'refreshToken');
        if (refreshToken != null) {
            print("The current refresh token is : $refreshToken");
            _isRefreshing = true;
            try {
                final newTokens = await _refreshAccessToken(refreshToken);
                if (newTokens != null) {
                    await _secureStorage.write(
                        key: 'accessToken',
                        value: newTokens['accessToken'],
                    );
                    await _secureStorage.write(
                        key: 'refreshToken',
                        value: newTokens['refreshToken'],
                    );

                    // Retry the original request with the new access token
                    final requestOptions = err.requestOptions;
                    requestOptions.headers['Authorization'] =
                        'Bearer ${newTokens['accessToken']}';
                    final response = await _dio.fetch(requestOptions);
                    handler.resolve(response);

                    // Process all pending requests
                    await _processPendingRequests(newTokens['accessToken']!);
                    return;
                }
            } on DioException catch (e) {
                print('🔴 Token refresh failed (DioException): $e');
                await _logOut();
                handler.reject(err);
                return;
            } catch (e) {
                print('🔴 Token refresh failed: $e');
                await _logOut();
            } finally {
                _isRefreshing = false;
            }
        }
        await _logOut(); // No refresh token or refresh failed
        handler.reject(err);
    } else {
        super.onError(err, handler);
    }
}

Future<Map<String, String>?> _refreshAccessToken(String refreshToken) async {
    final response = await _tokenDio
        .get(
            RefreshToken,
            options: Options(headers: {'Authorization': 'Bearer $refreshToken'}),
        )
        .timeout(Duration(seconds: 20));

    final data = response.data as Map<String, dynamic>;
    print('✅ Token refreshed successfully');
    print('🔑 New accessToken: ${data['accessToken']}');
    print('♻️ New refreshToken: ${data['refreshToken']}');
    return {
        'accessToken': data['accessToken'] as String,
        'refreshToken': data['refreshToken'] as String,
    };
}

Future<void> _processPendingRequests(String newAccessToken) async {
    print('🔄 Processing ${_pendingRequests.length} pending requests...');
    for (final requestOptions in _pendingRequests) {
        requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';
        await _dio.fetch(requestOptions);
    }
    _pendingRequests.clear();
}

Future<void> _logOut() async {
    await AuthService.logout();
    _pendingRequests.clear();
    _isRefreshing = false;
}
}

1 Answer 1

0

Here is how I handle the onError method in-order to handle 401 and 403 statuses.

onError: (e, handler) async {
        try {
          bool isForbidden = e.response?.statusCode == StatusCode.forbidden.value;
          bool isUnauthorized = e.response?.statusCode == StatusCode.unauthorized.value;
          if (isForbidden || isUnauthorized) {
            cancelToken.cancel("401 or 403 error occurred");
            // If a 401 response is received, refresh the access token
            String? accessToken = await _storage.getAccessToken();
            String? refreshToken = await _storage.getRefreshToken();
            if (accessToken == null || refreshToken == null) {
              _handleUserExit("No Access Token or Refresh Token");
              return;
            }

            /// We need the old tokens to request a refresh.
            bool result = await AuthEndpoint().refresh(accessToken, refreshToken);

            /// If refresh failed, abort.
            if (!result) {
              _handleUserExit("Refresh result:$result");
              return;
            }

            // Update the request header with the new access token
            String? newAccessToken = await _storage.getAccessToken();
            e.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';

            // Repeat the request with the updated header (?)
            // Repeating the request causes an infinite loop.
            /// return handler.reject(e);
            return handler.next(e);
            // return handler.resolve(await _dio.fetch(e.requestOptions));
          }
          return handler.next(e);
        } catch (e) {
          _handleUserExit("Refresh result:${e.toString()}");
        }
      },
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.