Skip to content

Commit 93cda90

Browse files
authored
refactor(dart): update the token storage to store a map of access token instance (#17)
1 parent c7d6f60 commit 93cda90

13 files changed

+295
-148
lines changed

lib/logto_client.dart

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import 'package:flutter_web_auth/flutter_web_auth.dart';
33
import 'package:http/http.dart' as http;
44
import 'package:jose/jose.dart';
55

6-
import '/logto_core.dart' as logto_core;
6+
import 'logto_core.dart' as logto_core;
77
import '/src/exceptions/logto_auth_exceptions.dart';
88
import '/src/interfaces/logto_interfaces.dart';
9+
import '/src/modules/id_token.dart';
10+
import '/src/modules/logto_storage_strategy.dart';
11+
import '/src/modules/pkce.dart';
12+
import '/src/modules/token_storage.dart';
13+
914
import '/src/utilities/constants.dart';
10-
import '/src/utilities/id_token.dart';
11-
import '/src/utilities/logto_storage_strategy.dart';
12-
import '/src/utilities/pkce.dart';
13-
import '/src/utilities/token_storage.dart';
1415
import '/src/utilities/utils.dart' as utils;
1516

1617
export '/src/interfaces/logto_config.dart';
@@ -148,10 +149,10 @@ class LogtoClient {
148149
}
149150

150151
await _tokenStorage.save(
151-
idToken: idToken,
152-
accessToken: tokenResponse.accessToken,
153-
refreshToken: tokenResponse.refreshToken,
154-
);
152+
idToken: idToken,
153+
accessToken: tokenResponse.accessToken,
154+
refreshToken: tokenResponse.refreshToken,
155+
expiresIn: tokenResponse.expiresIn);
155156
}
156157

157158
Future<void> signOut({

lib/src/interfaces/access_token.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:json_annotation/json_annotation.dart';
2+
3+
part 'access_token.g.dart';
4+
5+
@JsonSerializable()
6+
class AccessToken {
7+
@JsonKey(name: 'token', required: true, disallowNullValue: true)
8+
final String token;
9+
@JsonKey(name: 'scope', required: true, disallowNullValue: true)
10+
final String scope;
11+
@JsonKey(name: 'expiresAt', required: true, disallowNullValue: true)
12+
final DateTime expiresAt;
13+
14+
AccessToken({
15+
required this.token,
16+
required this.scope,
17+
required this.expiresAt,
18+
});
19+
20+
bool get isExpired => DateTime.now().toUtc().compareTo(expiresAt) > 0;
21+
22+
factory AccessToken.fromJson(Map<String, dynamic> json) =>
23+
_$AccessTokenFromJson(json);
24+
25+
Map<String, dynamic> toJson() => _$AccessTokenToJson(this);
26+
}

lib/src/interfaces/access_token.g.dart

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/src/interfaces/logto_interfaces.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export 'logto_code_token_response.dart';
22
export 'logto_config.dart';
33
export 'logto_refresh_token_response.dart';
44
export 'oidc_provider_config.dart';
5+
export 'access_token.dart';
File renamed without changes.

lib/src/utilities/pkce.dart renamed to lib/src/modules/pkce.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'dart:convert';
22

33
import 'package:crypto/crypto.dart';
44

5-
import 'utils.dart' as utils;
5+
import '/src/utilities/utils.dart' as utils;
66

77
class PKCE {
88
final String codeVerifier;

lib/src/modules/token_storage.dart

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import 'dart:convert';
2+
3+
import 'id_token.dart';
4+
import 'logto_storage_strategy.dart';
5+
import '/src/interfaces/access_token.dart';
6+
7+
class _TokenStorageKeys {
8+
static const accessTokenKey = 'logto_access_token';
9+
static const refreshTokenKey = 'logto_refresh_token';
10+
static const idTokenKey = 'logto_id_token';
11+
}
12+
13+
class TokenStorage {
14+
IdToken? _idToken;
15+
Map<String, AccessToken>? _accessTokenMap;
16+
String? _refreshToken;
17+
18+
late final LogtoStorageStrategy _storage;
19+
20+
static TokenStorage? loaded;
21+
22+
TokenStorage(
23+
LogtoStorageStrategy? storageStrategy,
24+
) {
25+
_storage = storageStrategy ?? SecureStorageStrategy();
26+
}
27+
28+
static IdToken? _decodeIdToken(String? encoded) {
29+
if (encoded == null) return null;
30+
return IdToken.unverified(encoded);
31+
}
32+
33+
static String? _encodeIdToken(IdToken? token) {
34+
return token?.toCompactSerialization();
35+
}
36+
37+
static String _encodeScopes(List<String>? scopes) {
38+
final List<String> scopeList = scopes ?? [];
39+
scopeList.sort();
40+
return scopeList.join(' ');
41+
}
42+
43+
Future<IdToken?> get idToken async {
44+
if (_idToken != null) {
45+
return _idToken!;
46+
}
47+
48+
final idTokenFromStorage =
49+
await _storage.read(key: _TokenStorageKeys.idTokenKey);
50+
_idToken = _decodeIdToken(idTokenFromStorage);
51+
52+
return _idToken;
53+
}
54+
55+
Future<void> setIdToken(IdToken? idToken) async {
56+
_idToken = idToken;
57+
58+
await _storage.write(
59+
key: _TokenStorageKeys.idTokenKey,
60+
value: _encodeIdToken(idToken),
61+
);
62+
}
63+
64+
static String _buildAccessTokenKey(String? resource,
65+
[List<String>? scopes]) =>
66+
"${_encodeScopes(scopes)}@${resource ?? ''}";
67+
68+
Future<Map<String, AccessToken>?> _getAccessTokenMapFromStorage() async {
69+
final tokenMapStorage =
70+
await _storage.read(key: _TokenStorageKeys.accessTokenKey);
71+
72+
if (tokenMapStorage != null) {
73+
try {
74+
final Map<String, dynamic> jsonMap = jsonDecode(tokenMapStorage);
75+
76+
return Map.fromEntries(jsonMap.keys
77+
.map((key) => MapEntry(key, AccessToken.fromJson(jsonMap[key]))));
78+
} catch (e) {
79+
return null;
80+
}
81+
}
82+
83+
return null;
84+
}
85+
86+
Future<AccessToken?> getAccessToken(
87+
[String? resource, List<String>? scopes]) async {
88+
final key = _buildAccessTokenKey(resource, scopes);
89+
90+
if (_accessTokenMap != null) {
91+
return _accessTokenMap![key];
92+
}
93+
94+
_accessTokenMap = await _getAccessTokenMapFromStorage();
95+
96+
return _accessTokenMap?[key];
97+
}
98+
99+
Future<void> _saveAccessTokenMapToStorage(
100+
Map<String, AccessToken> accessTokenMap) async {
101+
final jsonMap = Map.fromEntries(accessTokenMap.keys
102+
.map((key) => MapEntry(key, accessTokenMap[key]?.toJson())));
103+
await _storage.write(
104+
key: _TokenStorageKeys.accessTokenKey, value: jsonEncode(jsonMap));
105+
}
106+
107+
Future<void> setAccessToken(String accessToken,
108+
{String? resource, List<String>? scopes, required int expiresIn}) async {
109+
final key = _buildAccessTokenKey(resource, scopes);
110+
111+
final Map<String, AccessToken> newAccessTokenMap =
112+
Map.from(_accessTokenMap ?? {});
113+
114+
newAccessTokenMap.addAll({
115+
key: AccessToken(
116+
token: accessToken,
117+
scope: _encodeScopes(scopes),
118+
119+
/// convert the expireAt to standard utc time
120+
expiresAt: DateTime.now().add(Duration(seconds: expiresIn)).toUtc())
121+
});
122+
123+
await _saveAccessTokenMapToStorage(newAccessTokenMap);
124+
125+
_accessTokenMap = newAccessTokenMap;
126+
}
127+
128+
Future<String?> get refreshToken async {
129+
if (_refreshToken != null) {
130+
return _refreshToken;
131+
}
132+
133+
_refreshToken = await _storage.read(key: _TokenStorageKeys.refreshTokenKey);
134+
135+
return _refreshToken;
136+
}
137+
138+
Future<void> setRefreshToken(String? refreshToken) async {
139+
_refreshToken = refreshToken;
140+
141+
await _storage.write(
142+
key: _TokenStorageKeys.refreshTokenKey,
143+
value: refreshToken,
144+
);
145+
}
146+
147+
/// Initial token response saving
148+
///
149+
/// * required IdToken
150+
/// * required AccessToken
151+
/// * required expiresIn
152+
/// * optional refreshToken
153+
/// * initial AccessToken id for OpenID use only does not have resource & scope
154+
Future<void> save({
155+
required IdToken idToken,
156+
required String accessToken,
157+
String? refreshToken,
158+
required int expiresIn,
159+
}) async {
160+
await Future.wait([
161+
setAccessToken(accessToken, expiresIn: expiresIn),
162+
setIdToken(idToken),
163+
setRefreshToken(refreshToken),
164+
]);
165+
}
166+
167+
Future<void> clear() async {
168+
_accessTokenMap = null;
169+
_refreshToken = null;
170+
_idToken = null;
171+
await Future.wait<void>([
172+
_storage.delete(key: _TokenStorageKeys.accessTokenKey),
173+
_storage.delete(key: _TokenStorageKeys.refreshTokenKey),
174+
_storage.delete(key: _TokenStorageKeys.idTokenKey),
175+
]);
176+
}
177+
}

lib/src/utilities/token_storage.dart

Lines changed: 0 additions & 114 deletions
This file was deleted.

0 commit comments

Comments
 (0)