@@ -23,11 +23,19 @@ class LogtoClient {
2323
2424 static late TokenStorage _tokenStorage;
2525
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+
2632 /// Custom [http.Client] .
2733 ///
2834 /// Note that you will have to call `close()` yourself when passing a [http.Client] instance.
2935 late final http.Client ? _httpClient;
3036
37+ bool _loading = false ;
38+
3139 bool get loading => _loading;
3240
3341 OidcProviderConfig ? _oidcConfig;
@@ -66,15 +74,101 @@ class LogtoClient {
6674 return _oidcConfig! ;
6775 }
6876
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;
72101 }
73102
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+ }
75165
76166 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+
78172 final httpClient = _httpClient ?? http.Client ();
79173
80174 try {
@@ -130,21 +224,7 @@ class LogtoClient {
130224
131225 final idToken = IdToken .unverified (tokenResponse.idToken);
132226
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);
148228
149229 await _tokenStorage.save (
150230 idToken: idToken,
0 commit comments