Skip to content

Commit e9a78b3

Browse files
authored
Merge pull request #63 from nimblehq/feature/12-backend-refresh-token
[#12] [Backend] As a user, I can see the app automatically refreshes my expired access token
2 parents 141e044 + 4bfccf6 commit e9a78b3

File tree

11 files changed

+291
-37
lines changed

11 files changed

+291
-37
lines changed
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import 'package:dio/dio.dart';
2-
import 'package:survey_flutter/model/request/login_request.dart';
3-
import 'package:survey_flutter/model/response/login_response.dart';
42
import 'package:retrofit/retrofit.dart';
3+
import 'package:survey_flutter/model/request/login_request.dart';
4+
import 'package:survey_flutter/model/request/refresh_token_request.dart';
5+
import 'package:survey_flutter/model/response/token_response.dart';
56

67
part 'authentication_api_service.g.dart';
78

@@ -11,7 +12,12 @@ abstract class AuthenticationApiService {
1112
_AuthenticationApiService;
1213

1314
@POST('/oauth/token')
14-
Future<LoginResponse> login(
15-
@Body() LoginRequest body,
15+
Future<TokenResponse> login(
16+
@Body() LoginRequest loginRequest,
17+
);
18+
19+
@POST('/oauth/token')
20+
Future<TokenResponse> refreshToken(
21+
@Body() RefreshTokenRequest refreshTokenRequest,
1622
);
1723
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:survey_flutter/api/authentication_api_service.dart';
3+
import 'package:survey_flutter/di/provider/dio_provider.dart';
4+
import 'package:survey_flutter/env.dart';
5+
import 'package:survey_flutter/model/api_token.dart';
6+
import 'package:survey_flutter/model/request/refresh_token_request.dart';
7+
import 'package:survey_flutter/storage/secure_storage.dart';
8+
import 'package:survey_flutter/storage/secure_storage_impl.dart';
9+
10+
final tokenDataSourceProvider = Provider<TokenDataSource>((ref) {
11+
return TokenDataSourceImpl(ref.watch(secureStorageProvider),
12+
AuthenticationApiService(DioProvider().getDio()));
13+
});
14+
15+
abstract class TokenDataSource {
16+
Future<ApiToken> getToken({bool forceRefresh});
17+
Future<void> setToken(ApiToken token);
18+
}
19+
20+
class TokenDataSourceImpl extends TokenDataSource {
21+
final SecureStorage _secureStorage;
22+
final AuthenticationApiService _authenticationApiService;
23+
final String _grantType = 'refresh_token';
24+
25+
TokenDataSourceImpl(
26+
this._secureStorage,
27+
this._authenticationApiService,
28+
);
29+
30+
@override
31+
Future<ApiToken> getToken({bool forceRefresh = false}) async {
32+
if (forceRefresh) {
33+
final tokenResponse = await _authenticationApiService.refreshToken(
34+
RefreshTokenRequest(
35+
grantType: _grantType,
36+
clientId: Env.clientId,
37+
clientSecret: Env.clientSecret,
38+
),
39+
);
40+
final apiToken = tokenResponse.toApiToken();
41+
await _secureStorage.save(
42+
value: apiToken,
43+
key: SecureStorageKey.apiToken,
44+
);
45+
return apiToken;
46+
}
47+
48+
return await _secureStorage.getValue(key: SecureStorageKey.apiToken)
49+
as ApiToken;
50+
}
51+
52+
@override
53+
Future<void> setToken(ApiToken token) async {
54+
await _secureStorage.save(value: token, key: SecureStorageKey.apiToken);
55+
}
56+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import 'dart:io';
2+
3+
import 'package:dio/dio.dart';
4+
import 'package:survey_flutter/api/data_sources/token_data_source.dart';
5+
6+
const String _headerAuthorization = 'Authorization';
7+
const String _retryCountOption = 'Retry-Count';
8+
9+
class AuthInterceptor extends Interceptor {
10+
final Dio _dio;
11+
final TokenDataSource _tokenDataSource;
12+
13+
AuthInterceptor(
14+
this._dio,
15+
this._tokenDataSource,
16+
);
17+
18+
@override
19+
void onRequest(
20+
RequestOptions options,
21+
RequestInterceptorHandler handler,
22+
) async {
23+
final token = await _tokenDataSource.getToken();
24+
options.headers.putIfAbsent(
25+
_headerAuthorization, () => "${token.tokenType} ${token.accessToken}");
26+
super.onRequest(options, handler);
27+
}
28+
29+
@override
30+
void onError(
31+
DioError err,
32+
ErrorInterceptorHandler handler,
33+
) {
34+
final statusCode = err.response?.statusCode;
35+
final requestOptions = err.requestOptions;
36+
37+
if (statusCode != HttpStatus.forbidden &&
38+
statusCode != HttpStatus.unauthorized &&
39+
requestOptions.extra[_retryCountOption] != 1) {
40+
handler.next(err);
41+
return;
42+
}
43+
44+
requestOptions.extra[_retryCountOption] = 1;
45+
_refreshTokenAndRetry(requestOptions, handler);
46+
}
47+
48+
Future<void> _refreshTokenAndRetry(
49+
RequestOptions options,
50+
ErrorInterceptorHandler handler,
51+
) async {
52+
final token = await _tokenDataSource.getToken(forceRefresh: true);
53+
final headers = options.headers;
54+
headers[_headerAuthorization] = "${token.tokenType} ${token.accessToken}";
55+
final newOptions = options.copyWith(headers: headers);
56+
await _dio.fetch(newOptions).then((response) => handler.resolve(response));
57+
}
58+
}

lib/di/provider/dio_provider.dart

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,41 @@
11
import 'package:dio/dio.dart';
22
import 'package:flutter/foundation.dart';
3-
import 'package:survey_flutter/di/interceptor/app_interceptor.dart';
3+
import 'package:survey_flutter/api/data_sources/token_data_source.dart';
4+
import 'package:survey_flutter/api/interceptor/auth_interceptor.dart';
45
import 'package:survey_flutter/env.dart';
56

67
const String _headerContentType = 'Content-Type';
78
const String _defaultContentType = 'application/json; charset=utf-8';
89

910
class DioProvider {
1011
Dio? _dio;
12+
Dio? _authorizedDio;
13+
TokenDataSource? _tokenDataSource;
1114

1215
Dio getDio() {
1316
_dio ??= _createDio();
1417
return _dio!;
1518
}
1619

17-
Dio _createDio({bool requireAuthenticate = false}) {
20+
Dio getAuthorizedDio({
21+
required TokenDataSource tokenDataSource,
22+
}) {
23+
_tokenDataSource = tokenDataSource;
24+
_authorizedDio ??= _createDio(requireAuthentication: true);
25+
return _authorizedDio!;
26+
}
27+
28+
Dio _createDio({bool requireAuthentication = false}) {
1829
final dio = Dio();
19-
final appInterceptor = AppInterceptor(
20-
requireAuthenticate,
21-
dio,
22-
);
30+
2331
final interceptors = <Interceptor>[];
24-
interceptors.add(appInterceptor);
32+
if (requireAuthentication) {
33+
final authInterceptor = AuthInterceptor(
34+
dio,
35+
_tokenDataSource!,
36+
);
37+
interceptors.add(authInterceptor);
38+
}
2539
if (!kReleaseMode) {
2640
interceptors.add(LogInterceptor(
2741
request: true,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
3+
part 'refresh_token_request.g.dart';
4+
5+
@JsonSerializable()
6+
class RefreshTokenRequest {
7+
@JsonKey(name: 'grant_type')
8+
final String grantType;
9+
@JsonKey(name: 'client_id')
10+
final String clientId;
11+
@JsonKey(name: 'client_secret')
12+
final String clientSecret;
13+
14+
RefreshTokenRequest({
15+
required this.grantType,
16+
required this.clientId,
17+
required this.clientSecret,
18+
});
19+
20+
Map<String, dynamic> toJson() => _$RefreshTokenRequestToJson(this);
21+
}
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import 'package:survey_flutter/api/response_decoder.dart';
33
import 'package:survey_flutter/model/api_token.dart';
44
import 'package:survey_flutter/model/login_model.dart';
55

6-
part 'login_response.g.dart';
6+
part 'token_response.g.dart';
77

88
@JsonSerializable()
9-
class LoginResponse {
9+
class TokenResponse {
1010
final String id;
1111
final String accessToken;
1212
final String tokenType;
1313
final double expiresIn;
1414
final String refreshToken;
1515
final int createdAt;
1616

17-
LoginResponse({
17+
TokenResponse({
1818
required this.id,
1919
required this.accessToken,
2020
required this.tokenType,
@@ -23,8 +23,8 @@ class LoginResponse {
2323
required this.createdAt,
2424
});
2525

26-
factory LoginResponse.fromJson(Map<String, dynamic> json) =>
27-
_$LoginResponseFromJson(ResponseDecoder.decodeData(json));
26+
factory TokenResponse.fromJson(Map<String, dynamic> json) =>
27+
_$TokenResponseFromJson(ResponseDecoder.decodeData(json));
2828

2929
LoginModel toLoginModel() => LoginModel(
3030
id: id,
@@ -39,8 +39,8 @@ class LoginResponse {
3939
tokenType: tokenType,
4040
);
4141

42-
static LoginResponse dummy() {
43-
return LoginResponse(
42+
static TokenResponse dummy() {
43+
return TokenResponse(
4444
id: "",
4545
accessToken: "",
4646
tokenType: "",

lib/repositories/authentication_repository.dart

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import 'package:flutter_riverpod/flutter_riverpod.dart';
22
import 'package:survey_flutter/api/authentication_api_service.dart';
3+
import 'package:survey_flutter/api/data_sources/token_data_source.dart';
34
import 'package:survey_flutter/api/exception/network_exceptions.dart';
45
import 'package:survey_flutter/di/provider/dio_provider.dart';
56
import 'package:survey_flutter/env.dart';
67
import 'package:survey_flutter/model/login_model.dart';
78
import 'package:survey_flutter/model/request/login_request.dart';
8-
import 'package:survey_flutter/storage/secure_storage.dart';
9-
import 'package:survey_flutter/storage/secure_storage_impl.dart';
109

1110
const String _grantType = "password";
1211

1312
final authenticationRepositoryProvider =
1413
Provider<AuthenticationRepository>((ref) {
1514
return AuthenticationRepositoryImpl(
1615
AuthenticationApiService(DioProvider().getDio()),
17-
ref.watch(secureStorageProvider),
16+
ref.watch(tokenDataSourceProvider),
1817
);
1918
});
2019

@@ -27,11 +26,11 @@ abstract class AuthenticationRepository {
2726

2827
class AuthenticationRepositoryImpl extends AuthenticationRepository {
2928
final AuthenticationApiService _authenticationApiService;
30-
final SecureStorage _secureStorage;
29+
final TokenDataSource _tokenDataSource;
3130

3231
AuthenticationRepositoryImpl(
3332
this._authenticationApiService,
34-
this._secureStorage,
33+
this._tokenDataSource,
3534
);
3635

3736
@override
@@ -47,10 +46,7 @@ class AuthenticationRepositoryImpl extends AuthenticationRepository {
4746
clientSecret: Env.clientSecret,
4847
grantType: _grantType,
4948
));
50-
await _secureStorage.save(
51-
value: response.toApiToken(),
52-
key: SecureStorageKey.apiToken,
53-
);
49+
await _tokenDataSource.setToken(response.toApiToken());
5450
return response.toLoginModel();
5551
} catch (exception) {
5652
throw NetworkExceptions.fromDioException(exception);

lib/repositories/survey_repository.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:survey_flutter/api/data_sources/token_data_source.dart';
13
import 'package:survey_flutter/api/exception/network_exceptions.dart';
24
import 'package:survey_flutter/api/survey_api_service.dart';
5+
import 'package:survey_flutter/di/provider/dio_provider.dart';
36
import 'package:survey_flutter/model/surveys_container_model.dart';
47

8+
final surveyRepositoryProvider = Provider<SurveyRepository>((ref) {
9+
return SurveyRepositoryImpl(
10+
SurveyApiService(DioProvider().getAuthorizedDio(
11+
tokenDataSource: ref.watch(tokenDataSourceProvider),
12+
)),
13+
);
14+
});
15+
516
abstract class SurveyRepository {
617
Future<SurveysContainerModel> getSurveys({
718
required int pageNumber,

0 commit comments

Comments
 (0)