Skip to content

Instantly share code, notes, and snippets.

@iamnabink
Last active August 11, 2024 05:56
Show Gist options
  • Save iamnabink/306fbfadc54cefcaa90e654ff1f4fb9f to your computer and use it in GitHub Desktop.
Save iamnabink/306fbfadc54cefcaa90e654ff1f4fb9f to your computer and use it in GitHub Desktop.
Auth Refresh Token Retry Interceptor using Dio Package
/// 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