Skip to content

Commit b34d5f6

Browse files
authored
Merge pull request #63 from Bdaya-Dev/fix/refresh-the-cached-token
fix: Don't remove the cached tokens when they expire and instead attempt a refresh
2 parents 9649ee0 + b8d442e commit b34d5f6

File tree

10 files changed

+366
-136
lines changed

10 files changed

+366
-136
lines changed

docs/oidc-usage.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,15 @@ this is used for validating the JWT signature.
183183
### Initialization
184184
185185
After constructing the manager with the proper settings, you MUST call the `manager.init()` function, which will do the following:
186-
1. If the function has already been initialized (checked via the hasInit variable), it simply returns. This is because certain configurations and setups do not need, and should not be, repeated.
186+
187+
1. If the function has already been initialized (checked via the hasInit variable), it simply returns. This is because certain configurations and setups do not need to be repeated.
187188
2. initialize the passed store (calls `OidcStore.init()`)
188189
3. ensure that the discovery document has been retrieved (if the lazy constructor was used).
189190
4. if the discovery document contains a `jwks_uri` adds it the to the keystore.
190191
5. handle various state management tasks. It loads logout requests and state results (from samePage redirects), also attempts to load cached tokens if there are no state results or logout requests.
191-
6. Starts Listening to incoming Front Channel Logout requests.
192-
7. Starts listening to token expiry events for automatic refresh_token circulation.
192+
6. If the loaded token has expired and a refresh token exists, it attempts to refresh it, otherwise the token will be removed form the store.
193+
7. Starts Listening to incoming Front Channel Logout requests.
194+
8. Starts listening to token expiry events for automatic `refresh_token` circulation.
193195
194196
### Login
195197

packages/oidc/example/lib/access_token_usage_example.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import 'package:oidc/oidc.dart';
44

55
const kAuthorizationHeader = 'Authorization';
66
void _tryAppendAccessToken(
7-
OidcUserManager userManager, Map<String, dynamic> headers) {
7+
OidcUserManager userManager,
8+
Map<String, dynamic> headers,
9+
) {
810
if (headers.containsKey(kAuthorizationHeader)) {
911
// do nothing if header already exists.
1012
return;

packages/oidc/example/lib/pages/secret_page.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:convert';
2+
13
import 'package:flutter/foundation.dart';
24
import 'package:flutter/material.dart';
35
import 'package:go_router/go_router.dart';
@@ -199,7 +201,7 @@ class _SecretPageState extends State<SecretPage> {
199201
const Divider(),
200202
Text('id token: ${user.idToken}'),
201203
const Divider(),
202-
Text('token: ${user.token.toJson()}'),
204+
Text('token: ${jsonEncode(user.token.toJson())}'),
203205
],
204206
),
205207
),

packages/oidc/lib/src/managers/user_manager.dart

+62-19
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ class OidcUserManager {
534534
required String? nonce,
535535
required Map<String, dynamic>? attributes,
536536
required OidcProviderMetadata metadata,
537+
bool validateAndSave = true,
537538
}) async {
538539
final currentUser = this.currentUser;
539540
OidcUser newUser;
@@ -559,7 +560,11 @@ class OidcUserManager {
559560
'Server returned a wrong id_token nonce, might be a replay attack.',
560561
);
561562
}
562-
return _validateAndSaveUser(user: newUser, metadata: metadata);
563+
if (validateAndSave) {
564+
return _validateAndSaveUser(user: newUser, metadata: metadata);
565+
} else {
566+
return newUser;
567+
}
563568
}
564569

565570
Future<void> _saveUser(OidcUser user) async {
@@ -665,14 +670,15 @@ class OidcUserManager {
665670
),
666671
);
667672
return _createUserFromToken(
668-
token: OidcToken.fromResponse(
669-
tokenResponse,
670-
overrideExpiresIn: settings.getExpiresIn?.call(tokenResponse),
671-
sessionState: currentUser?.token.sessionState,
672-
),
673-
nonce: null,
674-
attributes: null,
675-
metadata: discoveryDocument);
673+
token: OidcToken.fromResponse(
674+
tokenResponse,
675+
overrideExpiresIn: settings.getExpiresIn?.call(tokenResponse),
676+
sessionState: currentUser?.token.sessionState,
677+
),
678+
nonce: null,
679+
attributes: null,
680+
metadata: discoveryDocument,
681+
);
676682
}
677683

678684
Future<void> _listenToTokenRefreshIfSupported(
@@ -741,29 +747,38 @@ class OidcUserManager {
741747
forgetUser();
742748
}
743749

744-
/// This function validates that a user claims
745-
Future<OidcUser?> _validateAndSaveUser({
750+
List<Exception> _validateUser({
746751
required OidcUser user,
747752
required OidcProviderMetadata metadata,
748-
}) async {
749-
var actualUser = user;
753+
}) {
750754
final errors = <Exception>[
751-
...actualUser.parsedIdToken.claims.validate(
755+
...user.parsedIdToken.claims.validate(
752756
clientId: clientCredentials.clientId,
753757
issuer: metadata.issuer,
754758
expiryTolerance: settings.expiryTolerance,
755759
),
756760
];
757-
if (actualUser.parsedIdToken.claims.subject == null) {
761+
if (user.parsedIdToken.claims.subject == null) {
758762
errors.add(
759763
JoseException('id token is missing a `sub` claim.'),
760764
);
761765
}
762-
if (actualUser.parsedIdToken.claims.issuedAt == null) {
766+
if (user.parsedIdToken.claims.issuedAt == null) {
763767
errors.add(
764768
JoseException('id token is missing an `iat` claim.'),
765769
);
766770
}
771+
772+
return errors;
773+
}
774+
775+
/// This function validates that a user claims
776+
Future<OidcUser?> _validateAndSaveUser({
777+
required OidcUser user,
778+
required OidcProviderMetadata metadata,
779+
}) async {
780+
var actualUser = user;
781+
final errors = _validateUser(user: actualUser, metadata: metadata);
767782
OidcUserInfoResponse? userInfoResp;
768783

769784
if (errors.isEmpty) {
@@ -804,7 +819,7 @@ class OidcUserManager {
804819
} else {
805820
for (final element in errors) {
806821
_logger.warning(
807-
'found a JWT, but failed the validation test: $element',
822+
'Found a JWT, but failed the validation test: $element',
808823
element,
809824
StackTrace.current,
810825
);
@@ -931,19 +946,47 @@ class OidcUserManager {
931946
if (rawToken == null) {
932947
return;
933948
}
949+
934950
try {
935951
final decodedAttributes = rawAttributes == null
936952
? null
937953
: jsonDecode(rawAttributes) as Map<String, dynamic>;
938954
final decodedToken = jsonDecode(rawToken) as Map<String, dynamic>;
939955
final token = OidcToken.fromJson(decodedToken);
940-
await _createUserFromToken(
956+
final metadata = discoveryDocument;
957+
var loadedUser = await _createUserFromToken(
941958
token: token,
942959
// nonce is only checked for new tokens.
943960
nonce: null,
944961
attributes: decodedAttributes,
945-
metadata: discoveryDocument,
962+
metadata: metadata,
963+
validateAndSave: false,
946964
);
965+
if (loadedUser != null) {
966+
final validationErrors = _validateUser(
967+
user: loadedUser,
968+
metadata: metadata,
969+
);
970+
final idTokenNeedsRefresh = validationErrors
971+
.whereType<JoseException>()
972+
.any((element) => element.message.startsWith('JWT expired'));
973+
if (token.refreshToken != null &&
974+
(idTokenNeedsRefresh || token.isAccessTokenExpired())) {
975+
loadedUser =
976+
await refreshToken(overrideRefreshToken: token.refreshToken);
977+
}
978+
if (loadedUser != null) {
979+
loadedUser = await _validateAndSaveUser(
980+
user: loadedUser,
981+
metadata: metadata,
982+
);
983+
}
984+
}
985+
986+
if (loadedUser == null) {
987+
_logAndThrow(
988+
'Found a cached token, but the user could not be created or validated');
989+
}
947990
} catch (e) {
948991
// remove invalid tokens, so that they don't get used again.
949992
await store.removeMany(

packages/oidc/lib/src/models/event.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:clock/clock.dart';
12
import 'package:oidc_core/oidc_core.dart';
23

34
/// Represents an arbitrary event.
@@ -8,7 +9,7 @@ abstract class OidcEvent {
89
});
910

1011
/// Creates an event whose [at] is now.
11-
OidcEvent.now() : at = DateTime.now();
12+
OidcEvent.now() : at = clock.now();
1213

1314
/// when the event occurred.
1415
final DateTime at;

packages/oidc/pubspec.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ dependencies:
3737
oidc_windows: ^0.3.1+1
3838
# ====================
3939
http: ^1.1.0
40-
jose_plus: ^0.4.3
40+
jose_plus: ^0.4.4
4141
retry: ^3.1.2
4242
rxdart: ^0.27.7
4343
logging: ^1.2.0
4444
json_annotation: ^4.8.1
4545
nonce: ^1.2.0
4646
uuid: ^4.0.0
47+
clock: ^1.1.1
4748

4849
dev_dependencies:
4950
build_runner: ^2.4.6

0 commit comments

Comments
 (0)