Skip to content

Commit

Permalink
fix: token doesn't refesh after expires (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
markgravity committed May 19, 2021
1 parent 012964b commit 57ba141
Show file tree
Hide file tree
Showing 24 changed files with 267 additions and 24 deletions.
7 changes: 2 additions & 5 deletions lib/gen/assets.gen.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions lib/modules/landing/landing_interactor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ part of 'landing_module.dart';

abstract class LandingInteractor extends Interactor<LandingInteractorDelegate> {
void validateAuthentication();
void logout();
}

abstract class LandingInteractorDelegate {
Expand All @@ -21,4 +22,9 @@ class LandingInteractorImpl extends LandingInteractor {
delegate?.authenticationDidFailToValidate.add(exception);
});
}

@override
void logout() {
_authRepository.logout().then((value) => null);
}
}
3 changes: 2 additions & 1 deletion lib/modules/landing/landing_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import 'package:survey/modules/home/home_module.dart';
import 'package:survey/modules/login/login_module.dart';
import 'package:survey/modules/screen.dart';
import 'package:survey/core/extensions/build_context.dart';
import 'package:survey/repositories/auth_repository.dart';
import 'package:survey/repositories/auth/auth_repository.dart';
import 'package:survey/services/api/api_service.dart';
import 'package:survey/services/locator/locator_service.dart';

part 'landing_presenter.dart';
Expand Down
10 changes: 8 additions & 2 deletions lib/modules/landing/landing_presenter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ class LandingPresenterImpl extends LandingPresenter
interactor.validateAuthentication();
}

void _authenticationDidFailToValidate(Object error) {
view.alert(error);
void _authenticationDidFailToValidate(Exception exception) {
if (exception == ApiException.invalidToken) {
interactor.logout();
router.replaceToLoginScreen(context: view.context);
return;
}

view.alert(exception);
}

void _didAllFinish(bool isAuthenticated) {
Expand Down
2 changes: 1 addition & 1 deletion lib/modules/login/login_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'package:survey/gen/assets.gen.dart';
import 'package:survey/modules/forgot_password/forgot_password_module.dart';
import 'package:survey/modules/home/home_module.dart';
import 'package:survey/modules/screen.dart';
import 'package:survey/repositories/auth_repository.dart';
import 'package:survey/repositories/auth/auth_repository.dart';
import 'package:survey/services/locator/locator_service.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Expand Down
25 changes: 25 additions & 0 deletions lib/repositories/auth/auth_refresh_token_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
part of 'auth_repository.dart';

class AuthRefreshTokenInterceptor extends HttpInterceptor {
final AuthRepository _authRepository = locator.get();

@override
final identifier = "auth_refresh_token";

@override
Future<void> onException(
HttpException exception, HttpExceptionInterceptorHandler handler) async {
final apiException = ApiException.fromHttpException(exception);
if (apiException == null || apiException != ApiException.invalidToken) {
return handler.next(exception);
}

try {
await _authRepository.refreshToken();
} on Exception {
return handler.next(exception);
}

return handler.retry(exception);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import 'package:survey/models/auth_token_info.dart';
import 'package:survey/services/api/api_service.dart';
import 'package:survey/services/api/auth/auth_api_service.dart';
import 'package:survey/services/api/user/user_api_service.dart';
import 'package:survey/services/http/http_service.dart';
import 'package:survey/services/local_storage/local_storage_service.dart';
import 'package:survey/services/locator/locator_service.dart';
import 'package:survey/models/user_info.dart';

part 'auth_refresh_token_interceptor.dart';

abstract class AuthRepository {
static const tokenLocalStorageKey = "auth_repository_token";

Expand All @@ -27,6 +30,8 @@ abstract class AuthRepository {
Future<void> fetchUser();

Future<void> attemptAndFetchUser();

Future<void> refreshToken();
}

class AuthRepositoryImpl implements AuthRepository {
Expand Down Expand Up @@ -83,6 +88,7 @@ class AuthRepositoryImpl implements AuthRepository {
}

_accessToken = token.accessToken;
_apiService.addGlobalInterceptors([AuthRefreshTokenInterceptor()]);
_apiService.configureGlobalToken(_accessToken, token.tokenType);
}

Expand All @@ -99,4 +105,18 @@ class AuthRepositoryImpl implements AuthRepository {
await attempt();
await fetchUser();
}

@override
Future<void> refreshToken() async {
final oldToken = await _localStorageService
.getObject<AuthTokenInfo>(AuthRepository.tokenLocalStorageKey);

final params =
AuthRefreshTokenParams(refreshToken: oldToken!.refreshToken!);
final token = await _authApiService.refreshToken(params: params);

await _localStorageService.setObject(token,
key: AuthRepository.tokenLocalStorageKey);
await attemptAndFetchUser();
}
}
14 changes: 12 additions & 2 deletions lib/services/api/api_exception.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
part of 'api_service.dart';

class ApiException implements LocalizedException {
class ApiException extends Equatable implements LocalizedException {
const ApiException({
required this.source,
required this.message,
Expand All @@ -26,13 +26,23 @@ class ApiException implements LocalizedException {
}

final String? source;
final String code;

@override
final String message;
final String code;

@override
List<Object?> get props => [source, code];

static const invalidResponseStructure = ApiException(
source: "local",
message: "Wrong response structure",
code: "wrong_response_structure",
);

static const invalidToken = ApiException(
source: "unauthorized",
message: "The access token is invalid",
code: "invalid_token",
);
}
14 changes: 14 additions & 0 deletions lib/services/api/api_service.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:survey/core/classes/localized_exception.dart';
import 'package:survey/gen/configs.gen.dart';
import 'package:survey/services/http/http_service.dart';
Expand Down Expand Up @@ -36,12 +37,16 @@ abstract class ApiService {
void configureGlobalBaseUrl(String? baseUrl);

void configureGlobalToken(String? token, String? tokenType);

void addGlobalInterceptors(List<HttpInterceptor> interceptor);
}

class ApiServiceImpl implements ApiService {
static String? _baseUrl;
static String? _token;
static String _tokenType = "Bearer";
static final List<HttpInterceptor> _interceptor = [];

final HttpService _httpService = locator.get();

@override
Expand Down Expand Up @@ -111,6 +116,14 @@ class ApiServiceImpl implements ApiService {
}
}

@override
void addGlobalInterceptors(List<HttpInterceptor> interceptor) {
final identifiers = interceptor.map((e) => e.identifier);
_interceptor
.removeWhere((element) => identifiers.contains(element.identifier));
_interceptor.addAll(interceptor);
}

Future<Map<String, dynamic>> _request({
required HttpMethod method,
String? baseUrl,
Expand Down Expand Up @@ -139,6 +152,7 @@ class ApiServiceImpl implements ApiService {
data: params?.toJson(),
url: url,
headers: headers,
interceptors: _interceptor,
) as Map<String, dynamic>;
} on HttpException catch (e) {
throw ApiException.fromHttpException(e) ?? e;
Expand Down
19 changes: 16 additions & 3 deletions lib/services/api/auth/auth_api_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import 'package:survey/services/http/http_service.dart';

part 'params/auth_login_params.dart';

part 'params/auth_refresh_token_params.dart';

abstract class AuthApiService {
static const loginEndpoint = "/oauth/token";
static const logoutEndpoint = "/oauth/revoke";
static const refreshTokenEndpoint = loginEndpoint;

static const preferenceTokenKey = "auth_service_preference_token";

Future<AuthTokenInfo> login({
required AuthLoginParams params,
});
Future<AuthTokenInfo> login({required AuthLoginParams params});

Future<void> logout();

Future<AuthTokenInfo> refreshToken({required AuthRefreshTokenParams params});
}

class AuthApiServiceImpl implements AuthApiService {
Expand All @@ -40,4 +44,13 @@ class AuthApiServiceImpl implements AuthApiService {
endpoint: AuthApiService.logoutEndpoint,
);
}

@override
Future<AuthTokenInfo> refreshToken({required AuthRefreshTokenParams params}) {
return _apiService.call(
method: HttpMethod.post,
endpoint: AuthApiService.refreshTokenEndpoint,
params: params,
);
}
}
36 changes: 36 additions & 0 deletions lib/services/api/auth/params/auth_refresh_token_params.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
part of '../auth_api_service.dart';

class AuthRefreshTokenParams extends ApiParams {
factory AuthRefreshTokenParams({
required String refreshToken,
String? clientId,
String? clientSecret,
}) {
return AuthRefreshTokenParams._(
refreshToken: refreshToken,
clientId: clientId ?? Configs.app.api.clientId,
clientSecret: clientSecret ?? Configs.app.api.clientSecret,
);
}

AuthRefreshTokenParams._({
required this.refreshToken,
required this.clientId,
required this.clientSecret,
});

String refreshToken;
String clientId;
String clientSecret;
String grantType = "refresh_token";

@override
void mapping(Mapper map) {
map<String>(
"refresh_token", refreshToken, (v) => refreshToken = v as String);
map<String>("client_id", clientId, (v) => clientId = v as String);
map<String>(
"client_secret", clientSecret, (v) => clientSecret = v as String);
map<String>("grant_type", grantType, (v) => grantType = v as String);
}
}
2 changes: 1 addition & 1 deletion lib/services/http/http_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class HttpException implements Exception {
return HttpException(
response: response,
type: type,
error: error.error,
error: error,
);
}

Expand Down
51 changes: 51 additions & 0 deletions lib/services/http/http_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
part of 'http_service.dart';

abstract class HttpInterceptor {
String get identifier;

void onException(
HttpException exception,
HttpExceptionInterceptorHandler handler,
) =>
handler.next(exception);

Interceptor toInterceptor(Dio dio) {
return InterceptorsWrapper(onError: (e, handler) {
onException(
HttpException.fromDioError(e),
HttpExceptionInterceptorHandler._(dio: dio, handler: handler),
);
});
}
}

class HttpExceptionInterceptorHandler {
const HttpExceptionInterceptorHandler._({
required Dio dio,
required ErrorInterceptorHandler handler,
}) : _dio = dio,
_handler = handler;

final ErrorInterceptorHandler _handler;
final Dio _dio;

void next(HttpException exception) {
_handler.next(exception.error as DioError);
}

void reject(HttpException exception) {
_handler.reject(exception.error as DioError);
}

Future<void> retry(HttpException exception) {
final requestOptions = (exception.error as DioError).requestOptions;

return _dio.request(requestOptions.path,
cancelToken: requestOptions.cancelToken,
data: requestOptions.data,
onReceiveProgress: requestOptions.onReceiveProgress,
onSendProgress: requestOptions.onSendProgress,
queryParameters: requestOptions.queryParameters,
options: requestOptions as Options);
}
}
10 changes: 10 additions & 0 deletions lib/services/http/http_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import 'package:enumerated_class/enumerated_class.dart';
import 'package:flutter/foundation.dart';

part 'http_method.dart';

part 'http_exception.dart';

part 'http_response.dart';

part 'http_interceptor.dart';

abstract class HttpService {
Future<dynamic> request({
required HttpMethod method,
dynamic data,
required String url,
Map<String, dynamic>? headers,
List<HttpInterceptor> interceptors = const [],
});
}

Expand All @@ -33,7 +38,12 @@ class HttpServiceImpl implements HttpService {
dynamic data,
required String url,
Map<String, dynamic>? headers,
List<HttpInterceptor> interceptors = const [],
}) async {
_dio.interceptors.addAll(interceptors.map(
(e) => e.toInterceptor(_dio),
));

final options = Options(method: method.rawValue, headers: headers);
try {
final response = await _dio.request(url,
Expand Down
Loading

0 comments on commit 57ba141

Please sign in to comment.