Skip to content

Commit

Permalink
Add autorenewal interceptor
Browse files Browse the repository at this point in the history
The tests are tragique but it seems good to have automatic
session renewal happen anywhere vs having to scatter it anytime
an authenticated endpoint is used.
  • Loading branch information
backspace committed Sep 30, 2024
1 parent 56753fe commit 40dec5e
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 95 deletions.
2 changes: 2 additions & 0 deletions waydowntown_app/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:geolocator/geolocator.dart';
import 'package:talker_dio_logger/talker_dio_logger.dart';
import 'package:talker_flutter/talker_flutter.dart';
import 'package:waydowntown/developer_tools.dart';
import 'package:waydowntown/refresh_token_interceptor.dart';
import 'package:waydowntown/routes/request_run_route.dart';
import 'package:waydowntown/widgets/session_widget.dart';

Expand Down Expand Up @@ -57,6 +58,7 @@ class _HomeState extends State<Home> {
},
));

dio.interceptors.add(RefreshTokenInterceptor(dio: dio));
dio.interceptors.add(TalkerDioLogger(
talker: talker,
settings: const TalkerDioLoggerSettings(
Expand Down
144 changes: 144 additions & 0 deletions waydowntown_app/lib/refresh_token_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Adapted from https://medium.com/@dariovarrialeapps/how-to-create-a-refresh-token-interceptor-in-flutter-with-dio-64a3ab0be6fa

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';

class RefreshTokenInterceptor extends InterceptorsWrapper {
final Dio dio;
final Dio? renewalDio;
final Dio? postRenewalDio;

RefreshTokenInterceptor({
required this.dio,
this.renewalDio,
this.postRenewalDio,
});

@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
_addTokenIfNeeded(options, handler);
}

@override
Future<void> onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
_debugPrint('### Error: ${err.response?.statusCode} ###');
if (err.response?.statusCode != 401) {
return handler.next(err);
}

_refreshTokenAndResolveError(err, handler);
}

/// Adds the user token to the request headers if it's not already there.
/// If the token is not present, the request will be sent without it.
///
/// If the token is present, it will be added to the headers.
void _addTokenIfNeeded(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
if (options.headers.containsKey('Authorization')) {
return handler.next(options);
}

final prefs = await SharedPreferences.getInstance();
final userToken = prefs.getString('access_token');

if (userToken != null && userToken.isNotEmpty) {
options.headers['Authorization'] = userToken;
}

handler.next(options);
}

/// Refreshes the user token and retries the request.
/// If the token refresh fails, the error will be passed to the next interceptor.
void _refreshTokenAndResolveError(
DioException err,
ErrorInterceptorHandler handler,
) async {
_debugPrint('### Refreshing token... ###');
final prefs = await SharedPreferences.getInstance();
final refreshToken = prefs.getString('renewal_token');

if (refreshToken == null) {
return handler.next(err);
}

late final Response authResponse;

Dio actualRenewalDio;

if (renewalDio == null) {
actualRenewalDio = Dio();
actualRenewalDio.options = dio.options;
print('renewal dio options: ${actualRenewalDio.options.baseUrl}');
} else {
print("using provided renewal dio");
actualRenewalDio = renewalDio!;
}

print("headers");
print({
'Authorization': refreshToken,
'Accept': 'application/json',
'Content-Type': 'application/json',
});

try {
authResponse = await actualRenewalDio.post(
'/powapi/session/renew',
options: Options(headers: {
'Authorization': refreshToken,
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
);
} catch (e) {
prefs.remove('access_token');
prefs.remove('renewal_token');

if (e is DioException) {
return handler.next(e);
}

return handler.next(err);
}

_debugPrint('### Token refreshed! ###');

await prefs.setString(
'access_token', authResponse.data['data']['access_token']);
await prefs.setString(
'renewal_token', authResponse.data['data']['renewal_token']);

err.requestOptions.headers['Authorization'] =
'${authResponse.data['data']['access_token']}';

Dio actualPostRenewalDio;

if (postRenewalDio == null) {
actualPostRenewalDio = Dio();
actualPostRenewalDio.options = dio.options;
} else {
actualPostRenewalDio = postRenewalDio!;
}

final refreshResponse =
await actualPostRenewalDio.fetch(err.requestOptions);
return handler.resolve(refreshResponse);
}

void _debugPrint(String message) {
if (kDebugMode) {
print(message);
}
}
}
85 changes: 25 additions & 60 deletions waydowntown_app/lib/widgets/session_widget.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:waydowntown/app.dart';
import 'package:waydowntown/tools/auth_form.dart';
import 'package:waydowntown/tools/my_specifications_table.dart';

Expand Down Expand Up @@ -29,82 +30,46 @@ class _SessionWidgetState extends State<SessionWidget> {
_isLoading = true;
});

final prefs = await SharedPreferences.getInstance();
final authToken = prefs.getString('access_token');

if (authToken != null) {
try {
final response = await _getSession(authToken);
if (response.statusCode == 200) {
setState(() {
_email = response.data['data']['attributes']['email'];
_isLoading = false;
});
return;
}
} catch (error) {
if (error is DioException && error.response?.statusCode == 401) {
final renewalToken = prefs.getString('renewal_token');
if (renewalToken != null) {
final renewedSession = await _renewSession(renewalToken);
if (renewedSession) {
await _checkSession();
return;
}
}
}
print('Error checking session: $error');
await _logout();
try {
final response = await _getSession();
if (response.statusCode == 200) {
setState(() {
_email = response.data['data']['attributes']['email'];
_isLoading = false;
});
return;
}
}
} catch (error) {
talker.error('Error checking session: $error');

setState(() {
_email = null;
_isLoading = false;
});
setState(() {
_email = null;
_isLoading = false;
});
}
}

Future<Response> _getSession(String authToken) {
Future<Response> _getSession() {
return widget.dio.get(
'${widget.apiBaseUrl}/fixme/session',
options: Options(headers: {
'Authorization': authToken,
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
);
}

Future<bool> _renewSession(String renewalToken) async {
try {
final response = await widget.dio.post(
'${widget.apiBaseUrl}/powapi/session/renew',
options: Options(headers: {
'Authorization': renewalToken,
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
);

if (response.statusCode == 200) {
final accessToken = response.data['data']['access_token'];
final newRenewalToken = response.data['data']['renewal_token'];

final prefs = await SharedPreferences.getInstance();
await prefs.setString('access_token', accessToken);
await prefs.setString('renewal_token', newRenewalToken);
return true;
}
} catch (error) {
print('Error renewing session: $error');
}
return false;
}

Future<void> _logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('access_token');
_checkSession();
await prefs.remove('renewal_token');

setState(() {
_email = null;
_isLoading = false;
});

await _checkSession();
}

void _openAuthForm() {
Expand Down
Loading

0 comments on commit 40dec5e

Please sign in to comment.