Skip to content

Commit 0f951e3

Browse files
authored
fix(auth): handle fallthrough exceptions in sign out state (#6226)
* updated signout function in the state machine to catch and return fallthrough exceptions, clearing the credential store when doing so. * added unit test
1 parent fc19d6a commit 0f951e3

File tree

6 files changed

+103
-6
lines changed

6 files changed

+103
-6
lines changed

packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,7 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
10741074
hostedUiException: result.hostedUiException,
10751075
globalSignOutException: result.globalSignOutException,
10761076
revokeTokenException: result.revokeTokenException,
1077+
invalidTokenException: result.invalidTokenException,
10771078
),
10781079
SignOutFailure(:final exception) => CognitoSignOutResult.failed(
10791080
AuthException.fromException(exception),

packages/auth/amplify_auth_cognito_dart/lib/src/model/signout/cognito_sign_out_result.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ sealed class CognitoSignOutResult extends SignOutResult
2626
HostedUiException? hostedUiException,
2727
GlobalSignOutException? globalSignOutException,
2828
RevokeTokenException? revokeTokenException,
29+
InvalidTokenException? invalidTokenException,
2930
}) = CognitoPartialSignOut._;
3031

3132
/// Whether credentials have been cleared from the local device.
@@ -85,6 +86,7 @@ final class CognitoPartialSignOut extends CognitoSignOutResult {
8586
this.hostedUiException,
8687
this.globalSignOutException,
8788
this.revokeTokenException,
89+
this.invalidTokenException,
8890
}) : super._();
8991

9092
/// The exception that occurred during Hosted UI sign out.
@@ -96,6 +98,9 @@ final class CognitoPartialSignOut extends CognitoSignOutResult {
9698
/// The exception that occurred while revoking the token.
9799
final RevokeTokenException? revokeTokenException;
98100

101+
/// The exception that occurred while signing out with an invalid userpool token.
102+
final InvalidTokenException? invalidTokenException;
103+
99104
@override
100105
bool get signedOutLocally => true;
101106

@@ -104,6 +109,7 @@ final class CognitoPartialSignOut extends CognitoSignOutResult {
104109
hostedUiException,
105110
globalSignOutException,
106111
revokeTokenException,
112+
invalidTokenException,
107113
signedOutLocally,
108114
];
109115

@@ -112,10 +118,26 @@ final class CognitoPartialSignOut extends CognitoSignOutResult {
112118
'hostedUiException': hostedUiException?.toString(),
113119
'globalSignOutException': globalSignOutException?.toString(),
114120
'revokeTokenException': revokeTokenException?.toString(),
121+
'invalidTokenException': invalidTokenException?.toString(),
115122
'signedOutLocally': signedOutLocally,
116123
};
117124
}
118125

126+
/// {@template amplify_auth_cognito_dart.model.signout.hosted_ui_exception}
127+
/// Exception thrown trying to sign out with an invalid userpool token (one or more of the Id, Access, or Refresh Token).
128+
/// {@endtemplate}
129+
class InvalidTokenException extends AuthServiceException {
130+
/// {@macro amplify_auth_cognito_dart.model.signout.invalid_token_exception}
131+
const InvalidTokenException({super.underlyingException})
132+
: super(
133+
'The provided user pool token is invalid',
134+
recoverySuggestion: 'See underlyingException for more details',
135+
);
136+
137+
@override
138+
String get runtimeTypeName => 'InvalidTokenException';
139+
}
140+
119141
/// {@template amplify_auth_cognito_dart.model.signout.hosted_ui_exception}
120142
/// Exception thrown trying to sign out of Hosted UI.
121143
/// {@endtemplate}

packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_out_state_machine.dart

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ final class SignOutStateMachine
7070
// Do not clear other storage items (e.g. AWS credentials) in this case,
7171
// since an unauthenticated user may still be cached.
7272
final CognitoUserPoolTokens tokens;
73+
74+
// Capture results of individual steps to determine overall success.
75+
HostedUiException? hostedUiException;
76+
GlobalSignOutException? globalSignOutException;
77+
RevokeTokenException? revokeTokenException;
78+
InvalidTokenException? invalidTokenException;
79+
7380
try {
7481
tokens = await manager.getUserPoolTokens();
7582
} on SignedOutException {
@@ -80,13 +87,20 @@ final class SignOutStateMachine
8087
// to clear the credentials associated with the non-existent user.
8188
await manager.clearCredentials();
8289
return emit(const SignOutState.success());
90+
} on Exception catch (e) {
91+
// unable to read tokens, clear the credentials to clear this invalid state.
92+
invalidTokenException = InvalidTokenException(underlyingException: e);
93+
await dispatchAndComplete(const CredentialStoreEvent.clearCredentials());
94+
return emit(
95+
SignOutState.partialFailure(
96+
hostedUiException: hostedUiException,
97+
globalSignOutException: globalSignOutException,
98+
revokeTokenException: revokeTokenException,
99+
invalidTokenException: invalidTokenException,
100+
),
101+
);
83102
}
84103

85-
// Capture results of individual steps to determine overall success.
86-
HostedUiException? hostedUiException;
87-
GlobalSignOutException? globalSignOutException;
88-
RevokeTokenException? revokeTokenException;
89-
90104
// Sign out via Hosted UI, if configured.
91105
Future<void> signOutHostedUi() async {
92106
if (tokens.signInMethod == CognitoSignInMethod.hostedUi) {
@@ -163,6 +177,7 @@ final class SignOutStateMachine
163177
hostedUiException: hostedUiException,
164178
globalSignOutException: globalSignOutException,
165179
revokeTokenException: revokeTokenException,
180+
invalidTokenException: invalidTokenException,
166181
),
167182
);
168183
}
@@ -177,6 +192,7 @@ final class SignOutStateMachine
177192
hostedUiException: hostedUiException,
178193
globalSignOutException: globalSignOutException,
179194
revokeTokenException: revokeTokenException,
195+
invalidTokenException: invalidTokenException,
180196
),
181197
);
182198
}

packages/auth/amplify_auth_cognito_dart/lib/src/state/state/sign_out_state.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ sealed class SignOutState extends AuthState<SignOutStateType> {
4040
HostedUiException? hostedUiException,
4141
GlobalSignOutException? globalSignOutException,
4242
RevokeTokenException? revokeTokenException,
43+
InvalidTokenException? invalidTokenException,
4344
}) = SignOutPartialFailure;
4445

4546
/// {@macro amplify_auth_cognito.sign_out_failure}
@@ -89,6 +90,7 @@ final class SignOutPartialFailure extends SignOutState {
8990
this.hostedUiException,
9091
this.globalSignOutException,
9192
this.revokeTokenException,
93+
this.invalidTokenException,
9294
}) : super._();
9395

9496
/// The exception that occurred during Hosted UI sign out.
@@ -100,6 +102,9 @@ final class SignOutPartialFailure extends SignOutState {
100102
/// The exception that occurred while revoking the token.
101103
final RevokeTokenException? revokeTokenException;
102104

105+
/// The exception that occurred while signing out with an invalid userpool token.
106+
final InvalidTokenException? invalidTokenException;
107+
103108
@override
104109
List<Object?> get props => [
105110
hostedUiException,

packages/auth/amplify_auth_cognito_test/test/plugin/sign_out_test.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:amplify_auth_cognito_dart/src/flows/hosted_ui/hosted_ui_platform
1010
import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart';
1111
import 'package:amplify_auth_cognito_dart/src/state/cognito_state_machine.dart';
1212
import 'package:amplify_auth_cognito_dart/src/state/state.dart';
13+
import 'package:amplify_auth_cognito_test/common/jwt.dart';
1314
import 'package:amplify_auth_cognito_test/common/mock_clients.dart';
1415
import 'package:amplify_auth_cognito_test/common/mock_config.dart';
1516
import 'package:amplify_auth_cognito_test/common/mock_hosted_ui.dart';
@@ -249,6 +250,57 @@ void main() {
249250
expect(hubEvents, emitsSignOutEvent);
250251
},
251252
);
253+
test(
254+
'clears credential store when signed in & token is invalid',
255+
() async {
256+
seedStorage(
257+
secureStorage,
258+
userPoolKeys: userPoolKeys,
259+
identityPoolKeys: identityPoolKeys,
260+
);
261+
final expiredIdToken = createJwt(
262+
type: TokenType.id,
263+
expiration: Duration.zero,
264+
);
265+
// Write an expired ID token to the secure storage
266+
secureStorage.write(
267+
key: userPoolKeys[CognitoUserPoolKey.idToken],
268+
value: expiredIdToken.raw,
269+
);
270+
await plugin.configure(
271+
config: mockConfig,
272+
authProviderRepo: testAuthRepo,
273+
);
274+
275+
final mockIdp = MockCognitoIdentityProviderClient(
276+
initiateAuth: (p0) async =>
277+
throw InternalErrorException(message: 'Invalid token'),
278+
);
279+
stateMachine.addInstance<CognitoIdentityProviderClient>(mockIdp);
280+
281+
await expectLater(
282+
plugin.signOut(),
283+
completion(
284+
isA<CognitoPartialSignOut>()
285+
.having(
286+
(res) => res.signedOutLocally,
287+
'signedOutLocally',
288+
isTrue,
289+
)
290+
.having(
291+
(res) => res.invalidTokenException,
292+
'invalidTokenException',
293+
isA<InvalidTokenException>(),
294+
),
295+
),
296+
);
297+
expect(
298+
plugin.stateMachine.getUserPoolTokens(),
299+
throwsSignedOutException,
300+
);
301+
expect(hubEvents, emitsSignOutEvent);
302+
},
303+
);
252304

253305
test('can sign out in user pool-only mode', () async {
254306
seedStorage(secureStorage, userPoolKeys: userPoolKeys);

packages/test/amplify_auth_integration_test/lib/src/test_runner.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,10 +305,11 @@ Future<void> signOutUser({bool assertComplete = false}) async {
305305
:final hostedUiException,
306306
:final globalSignOutException,
307307
:final revokeTokenException,
308+
:final invalidTokenException,
308309
):
309310
_logger.error(
310311
'Error signing out:',
311-
hostedUiException ?? globalSignOutException ?? revokeTokenException,
312+
hostedUiException ?? globalSignOutException ?? revokeTokenException ?? invalidTokenException,
312313
);
313314
case CognitoFailedSignOut(:final exception):
314315
_logger.error('Error signing out:', exception);

0 commit comments

Comments
 (0)