@@ -23,11 +23,19 @@ class LogtoClient {
23
23
24
24
static late TokenStorage _tokenStorage;
25
25
26
+ /// Logto automatically enables refresh token's rotation
27
+ ///
28
+ /// Simultaneous access token request may be problematic
29
+ /// Use a request cache map to avoid the race condition
30
+ static final Map <String , Future <AccessToken ?>> _accessTokenRequestCache = {};
31
+
26
32
/// Custom [http.Client] .
27
33
///
28
34
/// Note that you will have to call `close()` yourself when passing a [http.Client] instance.
29
35
late final http.Client ? _httpClient;
30
36
37
+ bool _loading = false ;
38
+
31
39
bool get loading => _loading;
32
40
33
41
OidcProviderConfig ? _oidcConfig;
@@ -66,15 +74,101 @@ class LogtoClient {
66
74
return _oidcConfig! ;
67
75
}
68
76
69
- Future <AccessToken ?> getAccessToken (
70
- {List <String >? scopes, String ? resource}) async {
71
- return await _tokenStorage.getAccessToken (resource, scopes);
77
+ Future <AccessToken ?> getAccessToken ({String ? resource}) async {
78
+ final accessToken = await _tokenStorage.getAccessToken (resource);
79
+
80
+ if (accessToken != null ) {
81
+ return accessToken;
82
+ }
83
+
84
+ // If no valid access token is found in storage, use refresh token to claim a new one
85
+ final cacheKey = TokenStorage .buildAccessTokenKey (resource);
86
+
87
+ // Reuse the cached request if is exist
88
+ if (_accessTokenRequestCache[cacheKey] != null ) {
89
+ return _accessTokenRequestCache[cacheKey];
90
+ }
91
+
92
+ // Create new token request and add it to cache
93
+ final newTokenRequest = _getAccessTokenByRefreshToken (resource);
94
+ _accessTokenRequestCache[cacheKey] = newTokenRequest;
95
+
96
+ final token = await newTokenRequest;
97
+ // Clear the cache after response
98
+ _accessTokenRequestCache.remove (cacheKey);
99
+
100
+ return token;
72
101
}
73
102
74
- bool _loading = false ;
103
+ // RBAC are not supported currently, no resource specific scopes are needed
104
+ Future <AccessToken ?> _getAccessTokenByRefreshToken (String ? resource) async {
105
+ final refreshToken = await _tokenStorage.refreshToken;
106
+
107
+ if (refreshToken == null ) {
108
+ throw LogtoAuthException (
109
+ LogtoAuthExceptions .authenticationError, 'not_authenticated' );
110
+ }
111
+
112
+ final httpClient = _httpClient ?? http.Client ();
113
+
114
+ try {
115
+ final oidcConfig = await _getOidcConfig (httpClient);
116
+
117
+ final response = await logto_core.fetchTokenByRefreshToken (
118
+ httpClient: httpClient,
119
+ tokenEndPoint: oidcConfig.tokenEndpoint,
120
+ clientId: config.appId,
121
+ refreshToken: refreshToken,
122
+ resource: resource,
123
+ // RBAC are not supported currently, no resource specific scopes are needed
124
+ scopes: resource != null ? ['offline_access' ] : null );
125
+
126
+ final scopes = response.scope.split (' ' );
127
+
128
+ await _tokenStorage.setAccessToken (response.accessToken,
129
+ expiresIn: response.expiresIn, resource: resource, scopes: scopes);
130
+
131
+ // renew refresh token
132
+ await _tokenStorage.setRefreshToken (response.refreshToken);
133
+
134
+ // verify and store id_token if not null
135
+ if (response.idToken != null ) {
136
+ final idToken = IdToken .unverified (response.idToken! );
137
+ await _verifyIdToken (idToken, oidcConfig);
138
+ await _tokenStorage.setIdToken (idToken);
139
+ }
140
+
141
+ return await _tokenStorage.getAccessToken (resource, scopes);
142
+ } finally {
143
+ if (_httpClient == null ) httpClient.close ();
144
+ }
145
+ }
146
+
147
+ Future <void > _verifyIdToken (
148
+ IdToken idToken, OidcProviderConfig oidcConfig) async {
149
+ final keyStore = JsonWebKeyStore ()
150
+ ..addKeySetUrl (Uri .parse (oidcConfig.jwksUri));
151
+
152
+ if (! await idToken.verify (keyStore)) {
153
+ throw LogtoAuthException (
154
+ LogtoAuthExceptions .idTokenValidationError, 'invalid jws signature' );
155
+ }
156
+
157
+ final violations = idToken.claims
158
+ .validate (issuer: Uri .parse (oidcConfig.issuer), clientId: config.appId);
159
+
160
+ if (violations.isNotEmpty) {
161
+ throw LogtoAuthException (
162
+ LogtoAuthExceptions .idTokenValidationError, '$violations ' );
163
+ }
164
+ }
75
165
76
166
Future <void > signIn (String redirectUri) async {
77
- if (_loading) throw Exception ('Already signing in...' );
167
+ if (_loading) {
168
+ throw LogtoAuthException (
169
+ LogtoAuthExceptions .isLoadingError, 'Already signing in...' );
170
+ }
171
+
78
172
final httpClient = _httpClient ?? http.Client ();
79
173
80
174
try {
@@ -130,21 +224,7 @@ class LogtoClient {
130
224
131
225
final idToken = IdToken .unverified (tokenResponse.idToken);
132
226
133
- final keyStore = JsonWebKeyStore ()
134
- ..addKeySetUrl (Uri .parse (oidcConfig.jwksUri));
135
-
136
- if (! await idToken.verify (keyStore)) {
137
- throw LogtoAuthException (
138
- LogtoAuthExceptions .idTokenValidationError, 'invalid jws signature' );
139
- }
140
-
141
- final violations = idToken.claims
142
- .validate (issuer: Uri .parse (oidcConfig.issuer), clientId: config.appId);
143
-
144
- if (violations.isNotEmpty) {
145
- throw LogtoAuthException (
146
- LogtoAuthExceptions .idTokenValidationError, '$violations ' );
147
- }
227
+ await _verifyIdToken (idToken, oidcConfig);
148
228
149
229
await _tokenStorage.save (
150
230
idToken: idToken,
0 commit comments