Last active
August 11, 2024 05:56
-
-
Save iamnabink/306fbfadc54cefcaa90e654ff1f4fb9f to your computer and use it in GitHub Desktop.
Auth Refresh Token Retry Interceptor using Dio Package
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// Author: Nabraj Khadka | |
/// Created: 28.05.2023 | |
/// Description: Auth Interceptor | |
/// | |
import 'package:project/app/api/interceptors/pretty_dio_logger.dart'; | |
import 'package:project/app/configs/api_endpoints.dart'; | |
import 'package:project/app/storage/storage_const.dart'; | |
import 'package:project/app/view/dialogs/toast.dart'; | |
import 'package:project/features/auth/domain/repositories/token_repository.dart'; | |
import 'package:project/helpers/dr_reboot.dart'; | |
import 'package:project/helpers/extensions/string_extension.dart'; | |
import 'package:project/routes/app_router.dart'; | |
import 'package:dio/dio.dart'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:platform_device_id/platform_device_id.dart'; | |
//TODO: Test this interceptor | |
class AuthInterceptor extends QueuedInterceptor { | |
AuthInterceptor({ | |
required this.dio, | |
required this.tokenRepository, | |
required this.appRouter, | |
}); | |
final Dio dio; | |
final TokenRepository tokenRepository; | |
final AppRouter appRouter; | |
/// isRefreshing flag is used to control whether a token refresh is already in progress. | |
/// | |
/// If it's in progress, subsequent requests will skip the refresh attempt | |
/// until the first one completes. | |
bool isRefreshing = false; | |
@override | |
Future<void> onRequest( | |
RequestOptions options, | |
RequestInterceptorHandler handler, | |
) async { | |
dio.options.headers[kDeviceIdentifier] = await PlatformDeviceId.getDeviceId; | |
if (options.headers[kIsTokenRequired] == false) { | |
// print('══ ✋🏻✋🏻 Refresh Token [Auth] Not required ✋🏻✋🏻══'); | |
// if the request doesn't need token, | |
// then just continue to the next interceptor | |
options.headers.remove(kIsTokenRequired); //remove the auxiliary header | |
options.headers.remove(kAuthorization); //remove the auxiliary header | |
handler.next(options); | |
} else { | |
// get tokens from local storage | |
final accessToken = await tokenRepository.token; | |
final refreshToken = await tokenRepository.refreshToken; | |
if (accessToken == null || refreshToken == null) { | |
// create custom dio error | |
final error = DioError( | |
error: 'Session expired please login'.rawString, | |
requestOptions: options, | |
response: Response<dynamic>( | |
statusMessage: 'Session expired please login'.rawString, | |
statusCode: 401, | |
requestOptions: options, | |
), | |
); | |
await _performLogout(dio); | |
return handler.reject(error); | |
} else { | |
options.headers.addAll( | |
<String, String>{kAuthorization: '$kBearer $accessToken'}, | |
); | |
handler.next(options); | |
} | |
} | |
} | |
@override | |
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async { | |
try { | |
if (err.requestOptions.headers[kIsTokenRequired] == true) { | |
if (err.response?.statusCode == 401) { | |
// if (dio.options.headers[kRetryCount] == 1) { | |
// await _performLogout(dio); | |
// return handler.next(err); | |
// } | |
// check if refreshing | |
if (!isRefreshing) { | |
isRefreshing = true; | |
final requestOptions = err.requestOptions; | |
final accessToken = await _refreshToken(); | |
if (accessToken == null) { | |
//if still access token is null means | |
await _performLogout(dio); | |
return handler.reject(err); | |
} else { | |
final opts = Options( | |
extra: err.requestOptions.extra, | |
method: requestOptions.method, | |
); | |
dio.options.headers[kAuthorization] = '$kBearer $accessToken'; | |
dio.options.headers[kAccept] = kApplicationJson; | |
// setting retry count to 1 to prevent further concurrent calls | |
// dio.options.headers[kRetryCount] = 1; | |
final response = await dio.request<dynamic>( | |
requestOptions.path, | |
options: opts, | |
cancelToken: requestOptions.cancelToken, | |
onReceiveProgress: requestOptions.onReceiveProgress, | |
data: requestOptions.data, | |
queryParameters: requestOptions.queryParameters, | |
); | |
if (response.statusCode != 401) { | |
// removing retry count after successful request | |
// dio.options.headers.remove(kRetryCount); | |
return handler.resolve(response); | |
} else { | |
return handler.reject(err); | |
} | |
} | |
} else { | |
return handler.next(err); | |
} | |
} else { | |
return handler.next(err); | |
} | |
} else { | |
return handler.next(err); | |
} | |
} catch (e) { | |
if (kDebugMode) { | |
print('🔴${e.toString()}🔴'); | |
} | |
return handler.reject( | |
err.copyWith( | |
error: e, | |
stackTrace: StackTrace.current, | |
), | |
); | |
} | |
} | |
Future<String?> _refreshToken() async { | |
try { | |
// should create new dioRefresh instance | |
// because the request interceptor is being locked | |
final dioRefresh = Dio(dio.options); | |
// get refresh token from local storage | |
final refreshToken = await tokenRepository.refreshToken; | |
// make request to server to get the new access token | |
// from server using refresh token | |
if (kDebugMode) dioRefresh.interceptors.add(PrettyDioLogger()); | |
final response = await dioRefresh.post<dynamic>( | |
APIEndpoints.refreshToken, | |
data: {kRefreshToken: refreshToken}, | |
options: Options( | |
headers: <String, dynamic>{ | |
kAccept: kApplicationJson, | |
}, | |
), | |
); | |
if (response.statusCode == 200 || response.statusCode == 201) { | |
//ignore: avoid_dynamic_calls | |
final newAccessToken = response.data['data'][kToken] | |
as String; // parse data based on your JSON structure | |
//ignore: avoid_dynamic_calls | |
// parse data based on your JSON structure | |
final newRefreshToken = response.data['data'][kRefreshToken] as String; | |
await tokenRepository.setToken(newAccessToken); | |
await tokenRepository.setRefreshToken( | |
newRefreshToken, | |
); // save to local storage | |
// save to local storage | |
return newAccessToken; | |
} else if (response.statusCode == 401) { | |
// it means your refresh token no longer valid now, | |
// it may be revoked by the backend | |
return null; | |
} else { | |
return null; | |
} | |
} on DioError { | |
return null; | |
} catch (e) { | |
return null; | |
} | |
} | |
Future<void> _performLogout(Dio dio) async { | |
// dio.options.headers.remove(kRetryCount); | |
// Since we have used route guard we do not need to logout manually | |
if (kDebugMode) print('LOGOUT CALLED'); | |
AppToast.warning('Session expired! Please re-login'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment