diff --git a/packages/amplify_core/lib/amplify_core.dart b/packages/amplify_core/lib/amplify_core.dart index aceb69a5a8..54667b4d05 100644 --- a/packages/amplify_core/lib/amplify_core.dart +++ b/packages/amplify_core/lib/amplify_core.dart @@ -81,6 +81,7 @@ export 'src/types/exception/codegen_exception.dart'; export 'src/types/exception/error/amplify_error.dart'; export 'src/types/exception/error/configuration_error.dart'; export 'src/types/exception/error/plugin_error.dart'; +export 'src/types/exception/network_exception.dart'; export 'src/types/exception/url_launcher_exception.dart'; /// Model-based types used in datastore and API diff --git a/packages/amplify_core/lib/src/types/exception/auth/auth_exception.dart b/packages/amplify_core/lib/src/types/exception/auth/auth_exception.dart index 57cf0ad693..178bc02ecc 100644 --- a/packages/amplify_core/lib/src/types/exception/auth/auth_exception.dart +++ b/packages/amplify_core/lib/src/types/exception/auth/auth_exception.dart @@ -28,6 +28,13 @@ abstract class AuthException extends AmplifyException with AWSDebuggable { underlyingException: e.underlyingException, ); } + if (e is AWSHttpException) { + return NetworkException( + 'The request failed due to a network error.', + recoverySuggestion: 'Ensure that you have an active network connection', + underlyingException: e, + ); + } String message; try { message = (e as dynamic).message as String; diff --git a/packages/amplify_core/lib/src/types/exception/network_exception.dart b/packages/amplify_core/lib/src/types/exception/network_exception.dart new file mode 100644 index 0000000000..27fcaadef6 --- /dev/null +++ b/packages/amplify_core/lib/src/types/exception/network_exception.dart @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; + +/// {@template amplify_core.network_exception} +/// Exception thrown when the requested operation fails due to a network +/// failure. +/// {@endtemplate} +class NetworkException extends AmplifyException + implements AuthException, StorageException { + /// {@macro amplify_core.network_exception} + const NetworkException( + super.message, { + super.recoverySuggestion, + super.underlyingException, + }); +} diff --git a/packages/amplify_test/lib/src/stubs/amplify_auth_cognito_stub.dart b/packages/amplify_test/lib/src/stubs/amplify_auth_cognito_stub.dart index a1923b07d3..8aeb5311af 100644 --- a/packages/amplify_test/lib/src/stubs/amplify_auth_cognito_stub.dart +++ b/packages/amplify_test/lib/src/stubs/amplify_auth_cognito_stub.dart @@ -1,9 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// ignore_for_file: depend_on_referenced_packages, implementation_imports, invalid_use_of_internal_member + import 'dart:core'; import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'package:amplify_auth_cognito_dart/src/jwt/jwt.dart'; +import 'package:amplify_auth_cognito_dart/src/model/auth_result.dart'; import 'package:amplify_core/amplify_core.dart'; const usernameExistsException = UsernameExistsException( @@ -206,7 +210,36 @@ class AmplifyAuthCognitoStub extends AuthPluginInterface Future fetchAuthSession({ AuthSessionOptions? options, }) async { - return CognitoAuthSession(isSignedIn: _isSignedIn()); + if (_currentUser == null) { + return const CognitoAuthSession( + isSignedIn: false, + userPoolTokensResult: AuthResult.error( + SignedOutException('There is no user signed in.'), + ), + userSubResult: AuthResult.error( + SignedOutException('There is no user signed in.'), + ), + credentialsResult: AuthResult.error( + UnknownException('credentials not available in mocks'), + ), + identityIdResult: AuthResult.error( + UnknownException('identityId not available in mocks'), + ), + ); + } + final userPoolTokens = _currentUser!.userPoolTokens; + final userSub = _currentUser!.sub; + return CognitoAuthSession( + isSignedIn: true, + userPoolTokensResult: AuthResult.success(userPoolTokens), + userSubResult: AuthResult.success(userSub), + credentialsResult: const AuthResult.error( + UnknownException('credentials not available in mocks'), + ), + identityIdResult: const AuthResult.error( + UnknownException('identityId not available in mocks'), + ), + ); } @override @@ -380,6 +413,36 @@ class MockCognitoUser { required this.email, }); + CognitoUserPoolTokens get userPoolTokens { + final accessToken = JsonWebToken( + header: const JsonWebHeader(algorithm: Algorithm.hmacSha256), + claims: JsonWebClaims( + subject: sub, + expiration: DateTime.now().add(const Duration(minutes: 60)), + customClaims: { + 'username': username, + }, + ), + signature: const [], + ); + const refreshToken = 'refreshToken'; + final idToken = JsonWebToken( + header: const JsonWebHeader(algorithm: Algorithm.hmacSha256), + claims: JsonWebClaims( + subject: sub, + customClaims: { + 'cognito:username': username, + }, + ), + signature: const [], + ); + return CognitoUserPoolTokens( + accessToken: accessToken, + refreshToken: refreshToken, + idToken: idToken, + ); + } + MockCognitoUser copyWith({ String? sub, String? username, diff --git a/packages/api/amplify_api/example/integration_test/graphql/iam_test.dart b/packages/api/amplify_api/example/integration_test/graphql/iam_test.dart index bbb1fb28a5..58c7efe1d5 100644 --- a/packages/api/amplify_api/example/integration_test/graphql/iam_test.dart +++ b/packages/api/amplify_api/example/integration_test/graphql/iam_test.dart @@ -212,13 +212,8 @@ void main({bool useExistingTestUser = false}) { // would do if that was the auth mode. final authSession = await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; - final accessToken = authSession.userPoolTokens?.accessToken.raw; - if (accessToken == null) { - throw const AuthNotAuthorizedException( - 'Could not get access token from cognito.', - recoverySuggestion: 'Ensure test user signed in.', - ); - } + final accessToken = + authSession.userPoolTokensResult.value.accessToken.raw; final headers = {AWSHeaders.authorization: accessToken}; final reqThatShouldWork = GraphQLRequest( document: reqThatFails.document, diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/custom_authorizer_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/custom_authorizer_test.dart index f931572c73..913d5a1ff3 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/custom_authorizer_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/custom_authorizer_test.dart @@ -54,7 +54,7 @@ void main() { final session = await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; - expect(session.userPoolTokens, isNotNull); + expect(session.userPoolTokensResult.value, isNotNull); final apiUrl = config.api!.awsPlugin!.values .singleWhere((e) => e.endpointType == EndpointType.rest) @@ -79,7 +79,8 @@ void main() { ), headers: { AWSHeaders.accept: 'application/json;charset=utf-8', - AWSHeaders.authorization: session.userPoolTokens!.idToken.raw, + AWSHeaders.authorization: + session.userPoolTokensResult.value.idToken.raw, }, body: utf8.encode(payload), ); @@ -115,10 +116,8 @@ void main() { final cognitoPlugin = Amplify.Auth.getPlugin( AmplifyAuthCognito.pluginKey, ); - final session = await cognitoPlugin.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ); - expect(session.credentials, isNotNull); + final session = await cognitoPlugin.fetchAuthSession(); + expect(session.credentialsResult.value, isNotNull); final restApi = config.api!.awsPlugin!.values .singleWhere((e) => e.endpointType == EndpointType.rest); @@ -170,10 +169,8 @@ void main() { final cognitoPlugin = Amplify.Auth.getPlugin( AmplifyAuthCognito.pluginKey, ); - final session = await cognitoPlugin.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ); - expect(session.credentials, isNotNull); + final session = await cognitoPlugin.fetchAuthSession(); + expect(session.credentialsResult.value, isNotNull); final restApi = config.api!.awsPlugin!.values .singleWhere((e) => e.endpointType == EndpointType.rest); @@ -226,10 +223,8 @@ void main() { final cognitoPlugin = Amplify.Auth.getPlugin( AmplifyAuthCognito.pluginKey, ); - final session = await cognitoPlugin.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ); - expect(session.credentials, isNotNull); + final session = await cognitoPlugin.fetchAuthSession(); + expect(session.credentialsResult.value, isNotNull); final restOperation = Amplify.API.post( '/', diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/federated_sign_in_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/federated_sign_in_test.dart index 686c88fc6c..d569701469 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/federated_sign_in_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/federated_sign_in_test.dart @@ -62,7 +62,7 @@ void main() { expect(signInResult.nextStep.signInStep, 'DONE'); final userPoolTokens = - (await cognitoPlugin.fetchAuthSession()).userPoolTokens!; + (await cognitoPlugin.fetchAuthSession()).userPoolTokensResult.value; // Clear but do not sign out so that tokens are still valid. // ignore: invalid_use_of_protected_member await cognitoPlugin.plugin.stateMachine.dispatch( @@ -105,29 +105,25 @@ void main() { asyncTest('replaces unauthenticated identity', (_) async { // Get unauthenticated identity - final unauthSession = await cognitoPlugin.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ); + final unauthSession = await cognitoPlugin.fetchAuthSession(); final authSession = await federateToIdentityPool(); expect( authSession.identityId, - unauthSession.identityId, + unauthSession.identityIdResult.value, reason: 'Should retain unauthenticated identity', ); expect( authSession.credentials, - isNot(unauthSession.credentials), + isNot(unauthSession.credentialsResult.value), reason: 'Should get new credentials', ); }); asyncTest('can specify identity ID', (_) async { // Get unauthenticated identity (doesn't matter, just need identity ID) - final unauthSession = await cognitoPlugin.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ); - final identityId = unauthSession.identityId!; + final unauthSession = await cognitoPlugin.fetchAuthSession(); + final identityId = unauthSession.identityIdResult.value; final signInResult = await cognitoPlugin.signIn( username: username, @@ -136,7 +132,7 @@ void main() { expect(signInResult.nextStep.signInStep, 'DONE'); final userPoolTokens = - (await cognitoPlugin.fetchAuthSession()).userPoolTokens!; + (await cognitoPlugin.fetchAuthSession()).userPoolTokensResult.value; // Clear but do not sign out so that tokens are still valid. // ignore: invalid_use_of_protected_member await cognitoPlugin.plugin.stateMachine.dispatch( @@ -176,23 +172,23 @@ void main() { }); asyncTest('can clear federation', (_) async { - await federateToIdentityPool(); + final federateToIdentityPoolResult = await federateToIdentityPool(); await expectLater( cognitoPlugin.clearFederationToIdentityPool(), completes, ); - final clearedSession = await cognitoPlugin.fetchAuthSession(); + final unauthSession = await cognitoPlugin.fetchAuthSession(); expect( - clearedSession.identityId, - isNull, - reason: 'Should clear session', + unauthSession.identityIdResult.value, + isNot(federateToIdentityPoolResult.identityId), + reason: 'Should clear session and refetch', ); expect( - clearedSession.credentials, - isNull, - reason: 'Should clear session', + unauthSession.credentialsResult.value, + isNot(federateToIdentityPoolResult.credentials), + reason: 'Should clear session and refetch', ); }); diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/fetch_auth_session_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/fetch_auth_session_test.dart index 5186092655..fd828f148c 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/fetch_auth_session_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/fetch_auth_session_test.dart @@ -19,64 +19,69 @@ void main() { final username = generateUsername(); final password = generatePassword(); - setUpAll(() async { - await configureAuth(); + group('unauthenticated access enabled', () { + setUpAll(() async { + await configureAuth(); - await adminCreateUser( - username, - password, - autoConfirm: true, - verifyAttributes: true, - ); - }); - - tearDownAll(Amplify.reset); - - setUp(() async { - await signOutUser(); - final res = await Amplify.Auth.signIn( - username: username, - password: password, - ); - expect(res.isSignedIn, isTrue); - }); + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: true, + ); + }); - asyncTest( - 'should return user credentials if getAWSCredentials is true', - (_) async { - final res = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ) as CognitoAuthSession; + tearDownAll(Amplify.reset); + setUp(() async { + await signOutUser(); + final res = await Amplify.Auth.signIn( + username: username, + password: password, + ); expect(res.isSignedIn, isTrue); - expect(isValidUserSub(res.userSub), isTrue); - expect(isValidIdentityId(res.identityId), isTrue); - expect(isValidAWSCredentials(res.credentials), isTrue); - expect(isValidAWSCognitoUserPoolTokens(res.userPoolTokens), isTrue); - }, - ); - - asyncTest( - 'should return user credentials without getAWSCredentials', - (_) async { - final res = await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; + }); - expect(res.isSignedIn, isTrue); - expect(isValidUserSub(res.userSub), isTrue); - expect(isValidIdentityId(res.identityId), isTrue); - expect(isValidAWSCredentials(res.credentials), isTrue); - expect(isValidAWSCognitoUserPoolTokens(res.userPoolTokens), isTrue); - }, - ); + asyncTest( + 'should return user credentials', + (_) async { + final res = + await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; + expect(res.isSignedIn, isTrue); + expect(isValidUserSub(res.userSubResult.value), isTrue); + expect(isValidIdentityId(res.identityIdResult.value), isTrue); + expect(isValidAWSCredentials(res.credentialsResult.value), isTrue); + expect( + isValidAWSCognitoUserPoolTokens(res.userPoolTokensResult.value), + isTrue, + ); + }, + ); - asyncTest( - 'should return isSignedIn as false if the user is signed out', - (_) async { - await Amplify.Auth.signOut(); + group('user is signed out', () { + asyncTest( + 'should return isSignedIn as false with credentials present', + (_) async { + await Amplify.Auth.signOut(); + final res = + await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; + expect(res.isSignedIn, isFalse); + expect( + () => res.userPoolTokensResult.value, + throwsA(isA()), + ); + expect( + () => res.userSubResult.value, + throwsA(isA()), + ); + expect(isValidIdentityId(res.identityIdResult.value), isTrue); + expect(isValidAWSCredentials(res.credentialsResult.value), isTrue); + }, + ); + }); + }); - final res = await Amplify.Auth.fetchAuthSession(); - expect(res.isSignedIn, isFalse); - }, - ); + // TODO(Jordan-Nelson): add tests for unauthenticated access NOT enabled + // and user pool only. }); } diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/force_refresh_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/force_refresh_test.dart index cb0ccfe57a..f48210ae97 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/force_refresh_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/force_refresh_test.dart @@ -20,10 +20,8 @@ void main() { final session = await Amplify.Auth.fetchAuthSession( options: CognitoSessionOptions(forceRefresh: forceRefresh), ) as CognitoAuthSession; - final idToken = session.userPoolTokens?.idToken; - expect(idToken, isNotNull, reason: 'User is logged in'); - - return idToken!.claims.customClaims; + final idToken = session.userPoolTokensResult.value.idToken; + return idToken.claims.customClaims; } group('Force refresh', () { diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/sign_in_sign_out_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/sign_in_sign_out_test.dart index fcadaba5dc..9ca7a6508c 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/sign_in_sign_out_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/sign_in_sign_out_test.dart @@ -94,9 +94,8 @@ void main() { asyncTest('identity ID should be the same between sessions', (_) async { // Get unauthenticated identity - final unauthSession = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ) as CognitoAuthSession; + final unauthSession = + await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; // Sign in { @@ -108,20 +107,22 @@ void main() { } // Get authenticated identity - final authSession = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ) as CognitoAuthSession; - final authenticatedIdentity = authSession.identityId; + final authSession = + await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; + final authenticatedIdentity = authSession.identityIdResult; expect( authenticatedIdentity, - isNot(unauthSession.identityId), + isNot(unauthSession.identityIdResult.value), reason: 'Unauthenticated identities should be distinct from authenticated ' 'identities, since unauthenticated identities are vended to all ' 'new devices when guest access is enabled but should converge to ' 'a singular authenticated identity across all devices', ); - expect(authSession.credentials, isNot(unauthSession.credentials)); + expect( + authSession.credentialsResult.value, + isNot(unauthSession.credentialsResult.value), + ); await Amplify.Auth.signOut(); { @@ -132,12 +133,11 @@ void main() { expect(signInRes.nextStep.signInStep, 'DONE'); } - final newSession = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ) as CognitoAuthSession; + final newSession = + await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; expect( - newSession.identityId, - authenticatedIdentity, + newSession.identityIdResult.value, + authenticatedIdentity.value, reason: 'Authenticated identity should be the same between sessions', ); }); diff --git a/packages/auth/amplify_auth_cognito/example/ios/unit_tests/NativeAuthPluginTests.swift b/packages/auth/amplify_auth_cognito/example/ios/unit_tests/NativeAuthPluginTests.swift index 93754940a9..6cd93d4889 100644 --- a/packages/auth/amplify_auth_cognito/example/ios/unit_tests/NativeAuthPluginTests.swift +++ b/packages/auth/amplify_auth_cognito/example/ios/unit_tests/NativeAuthPluginTests.swift @@ -51,7 +51,7 @@ class NativeAuthPluginTests: XCTestCase { let binaryMessenger = MockBinaryMessenger(isSignedIn: isSignedIn) let nativePlugin = NativeAuthPlugin(binaryMessenger: binaryMessenger) let expectation = expectation(description: "fetchAuthSession completes") - nativePlugin.fetchAuthSessionGetAwsCredentials(NSNumber(booleanLiteral: true)) { + nativePlugin.fetchAuthSession() { session, error in defer { expectation.fulfill() } diff --git a/packages/auth/amplify_auth_cognito/example/lib/main.dart b/packages/auth/amplify_auth_cognito/example/lib/main.dart index a60ef0a5c1..4ff50e7969 100644 --- a/packages/auth/amplify_auth_cognito/example/lib/main.dart +++ b/packages/auth/amplify_auth_cognito/example/lib/main.dart @@ -163,9 +163,7 @@ class _HomeScreenState extends State { } Future _fetchAuthSession() async { - final authSession = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ) as CognitoAuthSession; + final authSession = await Amplify.Auth.fetchAuthSession(); _logger.info( prettyPrintJson(authSession.toJson()), ); diff --git a/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart index 3fcd10549b..d4f974e57e 100644 --- a/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart +++ b/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart @@ -195,19 +195,15 @@ class _NativeAmplifyAuthCognito final CognitoAuthStateMachine _stateMachine; @override - Future fetchAuthSession( - bool getAwsCredentials, - ) async { + Future fetchAuthSession() async { try { - final authSession = await _basePlugin.fetchAuthSession( - options: CognitoSessionOptions(getAWSCredentials: getAwsCredentials), - ); + final authSession = await _basePlugin.fetchAuthSession(); final nativeAuthSession = NativeAuthSession( isSignedIn: authSession.isSignedIn, - userSub: authSession.userSub, - identityId: authSession.identityId, + userSub: authSession.userSubResult.valueOrNull, + identityId: authSession.identityIdResult.valueOrNull, ); - final userPoolTokens = authSession.userPoolTokens; + final userPoolTokens = authSession.userPoolTokensResult.valueOrNull; if (userPoolTokens != null) { nativeAuthSession.userPoolTokens = NativeUserPoolTokens( accessToken: userPoolTokens.accessToken.raw, @@ -215,7 +211,7 @@ class _NativeAmplifyAuthCognito idToken: userPoolTokens.idToken.raw, ); } - final awsCredentials = authSession.credentials; + final awsCredentials = authSession.credentialsResult.valueOrNull; if (awsCredentials != null) { nativeAuthSession.awsCredentials = NativeAWSCredentials( accessKeyId: awsCredentials.accessKeyId, diff --git a/packages/auth/amplify_auth_cognito/lib/src/native_auth_plugin.g.dart b/packages/auth/amplify_auth_cognito/lib/src/native_auth_plugin.g.dart index 78b075556e..cfa8460079 100644 --- a/packages/auth/amplify_auth_cognito/lib/src/native_auth_plugin.g.dart +++ b/packages/auth/amplify_auth_cognito/lib/src/native_auth_plugin.g.dart @@ -1,3 +1,4 @@ +// // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Autogenerated from Pigeon (v3.2.9), do not edit directly. @@ -226,7 +227,7 @@ abstract class NativeAuthPlugin { static const MessageCodec codec = _NativeAuthPluginCodec(); void exchange(Map params); - Future fetchAuthSession(bool getAwsCredentials); + Future fetchAuthSession(); static void setup(NativeAuthPlugin? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( @@ -256,14 +257,8 @@ abstract class NativeAuthPlugin { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.NativeAuthPlugin.fetchAuthSession was null.'); - final List args = (message as List?)!; - final bool? arg_getAwsCredentials = (args[0] as bool?); - assert(arg_getAwsCredentials != null, - 'Argument for dev.flutter.pigeon.NativeAuthPlugin.fetchAuthSession was null, expected non-null bool.'); - final NativeAuthSession output = - await api.fetchAuthSession(arg_getAwsCredentials!); + // ignore message + final NativeAuthSession output = await api.fetchAuthSession(); return output; }); } diff --git a/packages/auth/amplify_auth_cognito/pigeons/native_auth_plugin.dart b/packages/auth/amplify_auth_cognito/pigeons/native_auth_plugin.dart index e6a54fdc02..69abafd544 100644 --- a/packages/auth/amplify_auth_cognito/pigeons/native_auth_plugin.dart +++ b/packages/auth/amplify_auth_cognito/pigeons/native_auth_plugin.dart @@ -35,7 +35,7 @@ abstract class NativeAuthPlugin { void exchange(Map params); @async - NativeAuthSession fetchAuthSession(bool getAwsCredentials); + NativeAuthSession fetchAuthSession(); } @HostApi() diff --git a/packages/auth/amplify_auth_cognito_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPlugin.kt b/packages/auth/amplify_auth_cognito_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPlugin.kt index c7380217b9..4e9628c25f 100644 --- a/packages/auth/amplify_auth_cognito_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPlugin.kt +++ b/packages/auth/amplify_auth_cognito_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPlugin.kt @@ -77,7 +77,7 @@ class NativeAuthPlugin( return } MainScope().launch { - nativePlugin.fetchAuthSession(true) { session -> + nativePlugin.fetchAuthSession() { session -> val couldNotFetchException = UnknownException("Could not fetch") val userPoolTokens = if (session.userPoolTokens != null) { val tokens = FlutterFactory.createAWSCognitoUserPoolTokens( diff --git a/packages/auth/amplify_auth_cognito_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPluginBindingsPigeon.java b/packages/auth/amplify_auth_cognito_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPluginBindingsPigeon.java index e360d16e71..c5480da29f 100644 --- a/packages/auth/amplify_auth_cognito_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPluginBindingsPigeon.java +++ b/packages/auth/amplify_auth_cognito_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPluginBindingsPigeon.java @@ -1,3 +1,4 @@ +// // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Autogenerated from Pigeon (v3.2.9), do not edit directly. @@ -540,10 +541,10 @@ public void exchange(@NonNull Map paramsArg, Reply callbac callback.reply(null); }); } - public void fetchAuthSession(@NonNull Boolean getAwsCredentialsArg, Reply callback) { + public void fetchAuthSession(Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeAuthPlugin.fetchAuthSession", getCodec()); - channel.send(new ArrayList(Arrays.asList(getAwsCredentialsArg)), channelReply -> { + channel.send(null, channelReply -> { @SuppressWarnings("ConstantConditions") NativeAuthSession output = (NativeAuthSession)channelReply; callback.reply(output); diff --git a/packages/auth/amplify_auth_cognito_android/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPluginTests.kt b/packages/auth/amplify_auth_cognito_android/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPluginTests.kt index 20e986575f..1411dab33c 100644 --- a/packages/auth/amplify_auth_cognito_android/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPluginTests.kt +++ b/packages/auth/amplify_auth_cognito_android/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/NativeAuthPluginTests.kt @@ -10,6 +10,7 @@ import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.check +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -20,9 +21,9 @@ internal class NativeAuthPluginTests { val nativeAuthPlugin = NativeAuthPluginBindingsPigeon.NativeAuthPlugin(mockBinaryMessenger) val mockCallback = mock>() - nativeAuthPlugin.fetchAuthSession(true, mockCallback) + nativeAuthPlugin.fetchAuthSession(mockCallback) val callback = argumentCaptor() - verify(mockBinaryMessenger).send(any(), any(), callback.capture()) + verify(mockBinaryMessenger).send(any(), eq(null), callback.capture()) val codec = NativeAuthPluginBindingsPigeon.NativeAuthPlugin.getCodec() val authSession = NativeAuthPluginBindingsPigeon.NativeAuthSession.Builder().apply { setIsSignedIn(isSignedIn) diff --git a/packages/auth/amplify_auth_cognito_dart/example/bin/example.dart b/packages/auth/amplify_auth_cognito_dart/example/bin/example.dart index 6460a7d478..3a36c8b8e3 100644 --- a/packages/auth/amplify_auth_cognito_dart/example/bin/example.dart +++ b/packages/auth/amplify_auth_cognito_dart/example/bin/example.dart @@ -70,9 +70,13 @@ Future main() async { stdout ..writeln('Session Details') ..writeln('---------------') - ..writeln('Access Token: ${session.userPoolTokens?.accessToken.raw}') - ..writeln('Refresh Token: ${session.userPoolTokens?.refreshToken}') - ..writeln('ID Token: ${session.userPoolTokens?.idToken.raw}') + ..writeln( + 'Access Token: ${session.userPoolTokensResult.value.accessToken.raw}', + ) + ..writeln( + 'Refresh Token: ${session.userPoolTokensResult.value.refreshToken}', + ) + ..writeln('ID Token: ${session.userPoolTokensResult.value.idToken.raw}') ..writeln(); final attributes = await fetchUserAttributes(); diff --git a/packages/auth/amplify_auth_cognito_dart/example/lib/common.dart b/packages/auth/amplify_auth_cognito_dart/example/lib/common.dart index 4a8fd852e5..84d5005a59 100644 --- a/packages/auth/amplify_auth_cognito_dart/example/lib/common.dart +++ b/packages/auth/amplify_auth_cognito_dart/example/lib/common.dart @@ -75,11 +75,7 @@ Future changePassword({ } Future fetchAuthSession() async { - final res = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions( - getAWSCredentials: true, - ), - ); + final res = await Amplify.Auth.fetchAuthSession(); return res as CognitoAuthSession; } diff --git a/packages/auth/amplify_auth_cognito_dart/example/web/components/app_component.dart b/packages/auth/amplify_auth_cognito_dart/example/web/components/app_component.dart index 6d88dcbdc1..e185b5d0f4 100644 --- a/packages/auth/amplify_auth_cognito_dart/example/web/components/app_component.dart +++ b/packages/auth/amplify_auth_cognito_dart/example/web/components/app_component.dart @@ -84,11 +84,8 @@ class AppComponent extends StatefulComponent { AuthState startingAuthState; try { - final session = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions( - getAWSCredentials: true, - ), - ) as CognitoAuthSession; + final session = + await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; startingAuthState = session.isSignedIn ? AuthState.authenticated : AuthState.login; } on Exception { @@ -146,7 +143,9 @@ class AppComponent extends StatefulComponent { Future _fetchUnAuthCredentials() async { final session = await fetchAuthSession(); - safePrint('sessionToken : ${session.credentials?.sessionToken}'); + safePrint( + 'sessionToken : ${session.credentialsResult.value.sessionToken}', + ); } @override diff --git a/packages/auth/amplify_auth_cognito_dart/example/web/components/user_component.dart b/packages/auth/amplify_auth_cognito_dart/example/web/components/user_component.dart index 315f2f11f8..5eccceb11f 100644 --- a/packages/auth/amplify_auth_cognito_dart/example/web/components/user_component.dart +++ b/packages/auth/amplify_auth_cognito_dart/example/web/components/user_component.dart @@ -26,11 +26,14 @@ class UserComponent extends StatefulComponent { setState(() { _showSession = true; rows = [ - ['userSub', session.userSub ?? 'null'], - ['accessToken', session.userPoolTokens?.accessToken.raw ?? 'null'], - ['idToken', session.userPoolTokens?.idToken.raw ?? 'null'], - ['refreshToken', session.userPoolTokens?.refreshToken ?? 'null'], - ['credential session', session.credentials?.sessionToken ?? 'null'], + ['userSub', session.userSubResult.value], + ['accessToken', session.userPoolTokensResult.value.accessToken.raw], + ['idToken', session.userPoolTokensResult.value.idToken.raw], + ['refreshToken', session.userPoolTokensResult.value.refreshToken], + [ + 'credential session', + session.credentialsResult.value.sessionToken ?? 'null' + ], ...devices.map((device) => device.asCognitoDevice).map( (device) => [ 'Device: ${device.id}', diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart index 75077e0760..bafc4d8887 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart @@ -351,8 +351,8 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface< _stateMachine.dispatch(FetchAuthSessionEvent.federate(request)); final session = await fetchAuthSession(); return FederateToIdentityPoolResult( - identityId: session.identityId!, - credentials: session.credentials!, + identityId: session.identityIdResult.value, + credentials: session.credentialsResult.value, ); } @@ -1065,9 +1065,7 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface< if (_identityPoolConfig != null) { // Try to refresh AWS credentials since Cognito requests will require // them. - await fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ); + await fetchAuthSession(); if (options.globalSignOut) { // Revokes the refresh token try { @@ -1167,11 +1165,7 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface< @visibleForTesting Future getUserPoolTokens() async { final authSession = await fetchAuthSession(); - final userPoolTokens = authSession.userPoolTokens; - if (userPoolTokens == null) { - throw const SignedOutException('No user is currently signed in'); - } - return userPoolTokens; + return authSession.userPoolTokensResult.value; } @override diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/credentials/auth_plugin_credentials_provider.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/credentials/auth_plugin_credentials_provider.dart index ae427e7072..37c7ec2193 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/credentials/auth_plugin_credentials_provider.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/credentials/auth_plugin_credentials_provider.dart @@ -72,16 +72,11 @@ class AuthPluginCredentialsProviderImpl extends AuthPluginCredentialsProvider { // or refresh existing ones if needed, but do not initiate an // unauthenticated session since that should be handled via an explicit call // to `fetchAuthSession`. - await _dispatcher.dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: false), - ), - ); + await _dispatcher.dispatch(const FetchAuthSessionEvent.fetch()); final fetchState = await fetchAuthSessionMachine.getLatestResult(); - final fetchedCredentials = fetchState?.session.credentials; - if (fetchedCredentials == null) { + if (fetchState == null) { throw const InvalidStateException('Could not retrieve AWS credentials'); } - return fetchedCredentials; + return fetchState.session.credentialsResult.value; } } diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/model/auth_result.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/model/auth_result.dart new file mode 100644 index 0000000000..88a908d5ca --- /dev/null +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/model/auth_result.dart @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; + +/// {@template amplify_auth_cognito.model.auth_result} +/// The result of an Auth operation. +/// {@endtemplate} +class AuthResult extends AWSResult { + /// Creates a failed Auth result. + const AuthResult.error(super.exception, [super.stackTrace]) : super.error(); + + /// Creates a successful Auth result. + const AuthResult.success(super.value) : super.success(); +} diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_auth_session.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_auth_session.dart index d952c86324..c5fe17c8a9 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_auth_session.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_auth_session.dart @@ -2,15 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart'; +import 'package:amplify_auth_cognito_dart/src/model/auth_result.dart'; import 'package:amplify_core/amplify_core.dart'; -part 'cognito_auth_session.g.dart'; - /// {@template amplify_auth_cognito.model.cognito_auth_session} /// The current Cognito auth session, with AWS credentials and User Pool tokens /// of the active user. /// {@endtemplate} -@zAmplifySerializable class CognitoAuthSession extends AuthSession with AWSEquatable, @@ -18,36 +16,54 @@ class CognitoAuthSession extends AuthSession /// {@macro amplify_auth_cognito.model.cognito_auth_session} const CognitoAuthSession({ required super.isSignedIn, - this.credentials, - this.userPoolTokens, - this.userSub, - this.identityId, + required this.userPoolTokensResult, + required this.userSubResult, + required this.credentialsResult, + required this.identityIdResult, }); - /// {@macro amplify_auth_cognito.model.cognito_auth_session} - factory CognitoAuthSession.fromJson(Map json) => - _$CognitoAuthSessionFromJson(json); + /// The User Pool tokens Result. + final AuthResult userPoolTokensResult; + + /// The user ID (subject) Result. + final AuthResult userSubResult; + + /// The AWS credentials Result. + final AuthResult credentialsResult; + + /// The AWS identity ID Result. + final AuthResult identityIdResult; /// The AWS credentials. - final AWSCredentials? credentials; + @Deprecated('Use `credentialsResult.value` instead') + AWSCredentials? get credentials => credentialsResult.valueOrNull; /// The User Pool tokens. - final CognitoUserPoolTokens? userPoolTokens; + @Deprecated('Use `userPoolTokensResult.value` instead') + CognitoUserPoolTokens? get userPoolTokens => userPoolTokensResult.valueOrNull; /// The user ID (subject). - final String? userSub; + @Deprecated('Use `userSubResult.value` instead') + String? get userSub => userSubResult.valueOrNull; /// The AWS identity ID. - final String? identityId; + @Deprecated('Use `identityIdResult.value` instead') + String? get identityId => identityIdResult.valueOrNull; @override List get props => [ - credentials, - userPoolTokens, - userSub, - identityId, + userPoolTokensResult, + userSubResult, + credentialsResult, + identityIdResult, ]; @override - Map toJson() => _$CognitoAuthSessionToJson(this); + Map toJson() => { + 'isSignedIn': isSignedIn, + 'userSub': userSubResult.valueOrNull, + 'userPoolTokens': userPoolTokensResult.valueOrNull, + 'credentials': credentialsResult.valueOrNull, + 'identityId': identityIdResult.valueOrNull, + }; } diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_auth_session.g.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_auth_session.g.dart deleted file mode 100644 index 64da62895e..0000000000 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_auth_session.g.dart +++ /dev/null @@ -1,40 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'cognito_auth_session.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -CognitoAuthSession _$CognitoAuthSessionFromJson(Map json) => - CognitoAuthSession( - isSignedIn: json['isSignedIn'] as bool, - credentials: json['credentials'] == null - ? null - : AWSCredentials.fromJson( - json['credentials'] as Map), - userPoolTokens: json['userPoolTokens'] == null - ? null - : CognitoUserPoolTokens.fromJson( - json['userPoolTokens'] as Map), - userSub: json['userSub'] as String?, - identityId: json['identityId'] as String?, - ); - -Map _$CognitoAuthSessionToJson(CognitoAuthSession instance) { - final val = { - 'isSignedIn': instance.isSignedIn, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('credentials', instance.credentials?.toJson()); - writeNotNull('userPoolTokens', instance.userPoolTokens?.toJson()); - writeNotNull('userSub', instance.userSub); - writeNotNull('identityId', instance.identityId); - return val; -} diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_session_options.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_session_options.dart index 8a4b53d128..1aeeb84756 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_session_options.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_session_options.dart @@ -5,6 +5,10 @@ import 'package:amplify_core/amplify_core.dart'; part 'cognito_session_options.g.dart'; +const _getAWSCredentialsDeprecation = '`getAWSCredentials` is ignored. AWS ' + 'Credentials will always be retrieved. `credentialsResult` will contain ' + 'the result of retrieving credentials, which may be an error'; + /// {@template amplify_auth_cognito.model.cognito_session_options} /// Cognito options for `Amplify.Auth.fetchAuthSession`. /// {@endtemplate} @@ -13,7 +17,8 @@ class CognitoSessionOptions extends AuthSessionOptions with AWSEquatable, AWSDebuggable { /// {@macro amplify_auth_cognito.model.cognito_session_options} const CognitoSessionOptions({ - this.getAWSCredentials = false, + // ignore: avoid_unused_constructor_parameters + @Deprecated(_getAWSCredentialsDeprecation) bool? getAWSCredentials = false, super.forceRefresh = false, }); @@ -21,17 +26,8 @@ class CognitoSessionOptions extends AuthSessionOptions factory CognitoSessionOptions.fromJson(Map json) => _$CognitoSessionOptionsFromJson(json); - /// Whether to retrieve AWS credentials as part of the session fetching. - /// - /// If no AWS credentials are currently present, and this is `true`, a new - /// set of temporary credentials will be requested using the registered - /// Cognito Identity Pool. - /// - /// Defaults to `false`. - final bool getAWSCredentials; - @override - List get props => [getAWSCredentials, forceRefresh]; + List get props => [forceRefresh]; @override String get runtimeTypeName => 'CognitoSessionOptions'; diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_session_options.g.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_session_options.g.dart index 98fb80f650..dfced6e764 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_session_options.g.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/model/session/cognito_session_options.g.dart @@ -9,7 +9,6 @@ part of 'cognito_session_options.dart'; CognitoSessionOptions _$CognitoSessionOptionsFromJson( Map json) => CognitoSessionOptions( - getAWSCredentials: json['getAWSCredentials'] as bool? ?? false, forceRefresh: json['forceRefresh'] as bool? ?? false, ); @@ -17,5 +16,4 @@ Map _$CognitoSessionOptionsToJson( CognitoSessionOptions instance) => { 'forceRefresh': instance.forceRefresh, - 'getAWSCredentials': instance.getAWSCredentials, }; diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/fetch_auth_session_state_machine.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/fetch_auth_session_state_machine.dart index 886748dd07..5d2b2f9d53 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/fetch_auth_session_state_machine.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/fetch_auth_session_state_machine.dart @@ -8,6 +8,7 @@ import 'package:amplify_auth_cognito_dart/src/credentials/auth_plugin_credential import 'package:amplify_auth_cognito_dart/src/credentials/cognito_keys.dart'; import 'package:amplify_auth_cognito_dart/src/credentials/device_metadata_repository.dart'; import 'package:amplify_auth_cognito_dart/src/flows/constants.dart'; +import 'package:amplify_auth_cognito_dart/src/model/auth_result.dart'; import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity.dart' hide NotAuthorizedException; import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart' @@ -171,11 +172,8 @@ class FetchAuthSessionStateMachine extends FetchAuthSessionStateMachineBase { final userPoolTokens = result.data.userPoolTokens; final accessTokenExpiration = userPoolTokens?.accessToken.claims.expiration; final idTokenExpiration = userPoolTokens?.idToken.claims.expiration; - // Only force refresh user pool tokens when we have tokens to refresh and - // we are not also refreshing AWS credentials. - final forceRefreshUserPoolTokens = userPoolTokens != null && - options.forceRefresh && - !options.getAWSCredentials; + final forceRefreshUserPoolTokens = + userPoolTokens != null && options.forceRefresh; final refreshUserPoolTokens = forceRefreshUserPoolTokens || _isExpired(accessTokenExpiration) || _isExpired(idTokenExpiration); @@ -183,44 +181,66 @@ class FetchAuthSessionStateMachine extends FetchAuthSessionStateMachineBase { final hasIdentityPool = _identityPoolConfig != null; final awsCredentials = result.data.awsCredentials; final awsCredentialsExpiration = awsCredentials?.expiration; - // Only force a refresh of AWS credentials if `getAwsCredentials` is also - // true in order to allow the case of just refreshing the user pool tokens. - final forceRefreshAwsCredentials = - options.getAWSCredentials && options.forceRefresh; - final refreshAwsCredentials = - forceRefreshAwsCredentials || _isExpired(awsCredentialsExpiration); - final retrieveAwsCredentials = - awsCredentials == null && options.getAWSCredentials; - if ((refreshAwsCredentials || retrieveAwsCredentials) && !hasIdentityPool) { - throw const InvalidAccountTypeException.noIdentityPool( - recoverySuggestion: - 'Register an identity pool using the CLI or set getAWSCredentials ' - 'to false', - ); - } - - if (refreshUserPoolTokens || - refreshAwsCredentials || - retrieveAwsCredentials) { + final forceRefreshAwsCredentials = options.forceRefresh; + final retrieveAwsCredentials = awsCredentials == null; + final refreshAwsCredentials = hasIdentityPool && + (retrieveAwsCredentials || + forceRefreshAwsCredentials || + _isExpired(awsCredentialsExpiration)); + + if (refreshUserPoolTokens || refreshAwsCredentials) { dispatch( FetchAuthSessionEvent.refresh( refreshUserPoolTokens: refreshUserPoolTokens, - refreshAwsCredentials: - refreshAwsCredentials || retrieveAwsCredentials, + refreshAwsCredentials: refreshAwsCredentials, ), ); return; } // If refresh is not needed, return data directly from storage + final AuthResult credentialsResult; + final AuthResult identityIdResult; + if (hasIdentityPool) { + // awsCredentials & identityId cannot be null if refreshAwsCredentials is + // false. + credentialsResult = AuthResult.success(awsCredentials!); + identityIdResult = AuthResult.success(result.data.identityId!); + } else { + credentialsResult = const AuthResult.error( + InvalidAccountTypeException.noIdentityPool(), + ); + identityIdResult = const AuthResult.error( + InvalidAccountTypeException.noIdentityPool(), + ); + } + + final AuthResult userPoolTokensResult; + final AuthResult userSubResult; + if (userPoolTokens == null) { + userPoolTokensResult = const AuthResult.error( + SignedOutException('No user is currently signed in'), + ); + userSubResult = const AuthResult.error( + SignedOutException('No user is currently signed in'), + ); + } else { + userPoolTokensResult = AuthResult.success( + userPoolTokens, + ); + userSubResult = AuthResult.success( + userPoolTokens.userId, + ); + } + emit( FetchAuthSessionState.success( CognitoAuthSession( isSignedIn: userPoolTokens != null, - userPoolTokens: userPoolTokens, - credentials: awsCredentials, - userSub: userPoolTokens?.idToken.userId, - identityId: result.data.identityId, + userPoolTokensResult: userPoolTokensResult, + userSubResult: userSubResult, + credentialsResult: credentialsResult, + identityIdResult: identityIdResult, ), ), ); @@ -239,22 +259,38 @@ class FetchAuthSessionStateMachine extends FetchAuthSessionStateMachineBase { ); } - final awsCredentialsResult = await _retrieveAwsCredentials( - existingIdentityId: event.request.options?.developerProvidedIdentityId ?? - result.data.identityId, - federatedIdentity: _FederatedIdentity( - event.request.provider, - event.request.token, - ), - ); + AuthResult credentialsResult; + AuthResult identityIdResult; + + try { + final res = await _retrieveAwsCredentials( + existingIdentityId: + event.request.options?.developerProvidedIdentityId ?? + result.data.identityId, + federatedIdentity: _FederatedIdentity( + event.request.provider, + event.request.token, + ), + ); + credentialsResult = AuthResult.success(res.awsCredentials); + identityIdResult = AuthResult.success(res.identityId); + } on Exception catch (e, s) { + final authException = AuthException.fromException(e); + credentialsResult = AuthResult.error(authException, s); + identityIdResult = AuthResult.error(authException, s); + } dispatch( FetchAuthSessionEvent.succeeded( CognitoAuthSession( isSignedIn: userPoolTokens != null, - userPoolTokens: userPoolTokens, - userSub: userPoolTokens?.userId, - identityId: awsCredentialsResult.identityId, - credentials: awsCredentialsResult.awsCredentials, + userPoolTokensResult: const AuthResult.error( + SignedOutException('No user is currently signed in'), + ), + userSubResult: const AuthResult.error( + SignedOutException('No user is currently signed in'), + ), + identityIdResult: identityIdResult, + credentialsResult: credentialsResult, ), ), ); @@ -265,9 +301,12 @@ class FetchAuthSessionStateMachine extends FetchAuthSessionStateMachineBase { final result = await getOrCreate(CredentialStoreStateMachine.type) .getCredentialsResult(); + AuthResult userPoolTokensResult; + AuthResult userSubResult; + AuthResult credentialsResult; + AuthResult identityIdResult; + var userPoolTokens = result.data.userPoolTokens; - var identityId = result.data.identityId; - var awsCredentials = result.data.awsCredentials; if (event.refreshUserPoolTokens) { if (userPoolTokens == null) { dispatch( @@ -279,27 +318,71 @@ class FetchAuthSessionStateMachine extends FetchAuthSessionStateMachineBase { ); return; } - userPoolTokens = await _refreshUserPoolTokens(userPoolTokens); + try { + userPoolTokens = await _refreshUserPoolTokens(userPoolTokens); + userPoolTokensResult = AuthResult.success(userPoolTokens); + userSubResult = AuthResult.success(userPoolTokens.userId); + } on Exception catch (e, s) { + final authException = AuthException.fromException(e); + userPoolTokensResult = AuthResult.error(authException, s); + userSubResult = AuthResult.error(authException, s); + } + } else { + if (userPoolTokens != null) { + userPoolTokensResult = AuthResult.success(userPoolTokens); + userSubResult = AuthResult.success(userPoolTokens.userId); + } else { + userPoolTokensResult = const AuthResult.error( + SignedOutException('No user is currently signed in'), + ); + userSubResult = const AuthResult.error( + SignedOutException('No user is currently signed in'), + ); + } } - if (event.refreshAwsCredentials) { - final idToken = userPoolTokens?.idToken.raw; - final awsCredentialsResult = await _retrieveAwsCredentials( - existingIdentityId: identityId, - federatedIdentity: - idToken == null ? null : _FederatedIdentity.cognito(idToken), + + final existingIdentityId = result.data.identityId; + final existingAwsCredentials = result.data.awsCredentials; + + final hasIdentityPool = _identityPoolConfig != null; + + if (!hasIdentityPool) { + credentialsResult = const AuthResult.error( + InvalidAccountTypeException.noIdentityPool(), ); - identityId = awsCredentialsResult.identityId; - awsCredentials = awsCredentialsResult.awsCredentials; + identityIdResult = const AuthResult.error( + InvalidAccountTypeException.noIdentityPool(), + ); + } else if (event.refreshAwsCredentials) { + final idToken = userPoolTokens?.idToken.raw; + try { + final awsCredentialsResult = await _retrieveAwsCredentials( + existingIdentityId: existingIdentityId, + federatedIdentity: + idToken == null ? null : _FederatedIdentity.cognito(idToken), + ); + final identityId = awsCredentialsResult.identityId; + final awsCredentials = awsCredentialsResult.awsCredentials; + credentialsResult = AuthResult.success(awsCredentials); + identityIdResult = AuthResult.success(identityId); + } on Exception catch (e, s) { + final authException = AuthException.fromException(e); + credentialsResult = AuthResult.error(authException, s); + identityIdResult = AuthResult.error(authException, s); + } + } else { + credentialsResult = AuthResult.success(existingAwsCredentials!); + identityIdResult = AuthResult.success(existingIdentityId!); } dispatch( FetchAuthSessionEvent.succeeded( CognitoAuthSession( isSignedIn: userPoolTokens != null, - userPoolTokens: userPoolTokens, - userSub: userPoolTokens?.idToken.userId, - identityId: identityId, - credentials: awsCredentials, + userPoolTokensResult: userPoolTokensResult, + userSubResult: userSubResult, + identityIdResult: identityIdResult, + credentialsResult: credentialsResult, ), ), ); diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_in_state_machine.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_in_state_machine.dart index c4cd0fd03b..b083074154 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_in_state_machine.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_in_state_machine.dart @@ -613,11 +613,7 @@ class SignInStateMachine extends StateMachine { ), ); - await dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: true), - ), - ); + await dispatch(const FetchAuthSessionEvent.fetch()); // Wait for above to propagate and complete successfully. await expect(FetchAuthSessionStateMachine.type).getLatestResult(); diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart index e1a7ac7869..a3376f890c 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart @@ -21,16 +21,9 @@ class CognitoIamAuthProvider extends AWSIamAmplifyAuthProvider { /// AWS credentials from Auth category. @override Future retrieve() async { - final authSession = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions(getAWSCredentials: true), - ) as CognitoAuthSession; - final credentials = authSession.credentials; - if (credentials == null) { - throw const AuthNotAuthorizedException( - 'Unable to authorize request with IAM. No AWS credentials.', - ); - } - return credentials; + final authSession = + await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; + return authSession.credentialsResult.value; } /// Signs request with [AWSSigV4Signer] and AWS credentials from [retrieve]. diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart index d3c7f74385..7f6b9002ac 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart @@ -15,27 +15,13 @@ class CognitoUserPoolsAuthProvider extends TokenIdentityAmplifyAuthProvider { Future getLatestAuthToken() async { final authSession = await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; - final token = authSession.userPoolTokens?.accessToken.raw; - if (token == null) { - throw const AuthNotAuthorizedException( - 'Unable to fetch access token while authorizing with Cognito User Pools.', - ); - } - return token; + return authSession.userPoolTokensResult.value.accessToken.raw; } @override Future getIdentityId() async { final authSession = await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; - final identityId = authSession.identityId; - - if (identityId == null) { - throw const AuthNotAuthorizedException( - 'Unable to get identityId while authorizing with Cognito User Pools.', - ); - } - - return identityId; + return authSession.identityIdResult.value; } } diff --git a/packages/auth/amplify_auth_cognito_ios/ios/Classes/SwiftAuthCognito.swift b/packages/auth/amplify_auth_cognito_ios/ios/Classes/SwiftAuthCognito.swift index 839273068a..3bf0f591a2 100644 --- a/packages/auth/amplify_auth_cognito_ios/ios/Classes/SwiftAuthCognito.swift +++ b/packages/auth/amplify_auth_cognito_ios/ios/Classes/SwiftAuthCognito.swift @@ -147,7 +147,7 @@ public class SwiftAuthCognito: NSObject, FlutterPlugin, AuthCategoryPlugin, Nati request: AuthFetchSessionRequest(options: options ?? AuthFetchSessionRequest.Options()), resultListener: listener ) - nativeAuthPlugin.fetchAuthSessionGetAwsCredentials(true) { session, error in + nativeAuthPlugin.fetchAuthSession() { session, error in guard let session = session else { let authError: AuthError = .unknown( error?.localizedDescription ?? "Could not complete native request", diff --git a/packages/auth/amplify_auth_cognito_ios/ios/Classes/pigeons/NativeAuthPlugin.h b/packages/auth/amplify_auth_cognito_ios/ios/Classes/pigeons/NativeAuthPlugin.h index 7e57890062..e83597cf15 100644 --- a/packages/auth/amplify_auth_cognito_ios/ios/Classes/pigeons/NativeAuthPlugin.h +++ b/packages/auth/amplify_auth_cognito_ios/ios/Classes/pigeons/NativeAuthPlugin.h @@ -1,3 +1,4 @@ +// // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Autogenerated from Pigeon (v3.2.9), do not edit directly. @@ -89,7 +90,7 @@ NSObject *NativeAuthPluginGetCodec(void); @interface NativeAuthPlugin : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; - (void)exchangeParams:(NSDictionary *)params completion:(void(^)(NSError *_Nullable))completion; -- (void)fetchAuthSessionGetAwsCredentials:(NSNumber *)getAwsCredentials completion:(void(^)(NativeAuthSession *_Nullable, NSError *_Nullable))completion; +- (void)fetchAuthSessionWithCompletion:(void(^)(NativeAuthSession *_Nullable, NSError *_Nullable))completion; @end /// The codec used by NativeAuthBridge. NSObject *NativeAuthBridgeGetCodec(void); diff --git a/packages/auth/amplify_auth_cognito_ios/ios/Classes/pigeons/NativeAuthPlugin.m b/packages/auth/amplify_auth_cognito_ios/ios/Classes/pigeons/NativeAuthPlugin.m index 83a36bc2f9..afb787d162 100644 --- a/packages/auth/amplify_auth_cognito_ios/ios/Classes/pigeons/NativeAuthPlugin.m +++ b/packages/auth/amplify_auth_cognito_ios/ios/Classes/pigeons/NativeAuthPlugin.m @@ -1,3 +1,4 @@ +// // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Autogenerated from Pigeon (v3.2.9), do not edit directly. @@ -320,13 +321,13 @@ - (void)exchangeParams:(NSDictionary *)arg_params comple completion(nil); }]; } -- (void)fetchAuthSessionGetAwsCredentials:(NSNumber *)arg_getAwsCredentials completion:(void(^)(NativeAuthSession *_Nullable, NSError *_Nullable))completion { +- (void)fetchAuthSessionWithCompletion:(void(^)(NativeAuthSession *_Nullable, NSError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NativeAuthPlugin.fetchAuthSession" binaryMessenger:self.binaryMessenger codec:NativeAuthPluginGetCodec()]; - [channel sendMessage:@[arg_getAwsCredentials ?: [NSNull null]] reply:^(id reply) { + [channel sendMessage:nil reply:^(id reply) { NativeAuthSession *output = reply; completion(output, nil); }]; diff --git a/packages/auth/amplify_auth_cognito_test/test/credentials/auth_plugin_credentials_provider_test.dart b/packages/auth/amplify_auth_cognito_test/test/credentials/auth_plugin_credentials_provider_test.dart index 788b291c31..aeac2986e1 100644 --- a/packages/auth/amplify_auth_cognito_test/test/credentials/auth_plugin_credentials_provider_test.dart +++ b/packages/auth/amplify_auth_cognito_test/test/credentials/auth_plugin_credentials_provider_test.dart @@ -19,16 +19,6 @@ void main() { late AuthPluginCredentialsProviderImpl provider; late CognitoAuthStateMachine stateMachine; - // Performs the initial fetch so that credentials cache is hydrated. - Future fetchAuthSession() async { - await stateMachine.dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: true), - ), - ); - await Future.delayed(Duration.zero); - } - setUp(() async { stateMachine = CognitoAuthStateMachine() ..addBuilder(MockSecureStorage.new) @@ -54,17 +44,11 @@ void main() { ); }); - test('fails with no cached creds', () async { - expect(provider.retrieve(), throwsA(isA())); - }); - test('handles single request', () async { - await fetchAuthSession(); expect(provider.retrieve(), completion(isA())); }); test('handles concurrent requests', () async { - await fetchAuthSession(); final allCreds = await Future.wait( [ for (var i = 0; i < 10; i++) provider.retrieve(), @@ -84,7 +68,6 @@ void main() { }); test('fails when fetching from within state machine', () async { - await fetchAuthSession(); expect( runZoned( () => provider.retrieve(), diff --git a/packages/auth/amplify_auth_cognito_test/test/plugin/auth_providers_test.dart b/packages/auth/amplify_auth_cognito_test/test/plugin/auth_providers_test.dart index bbcf184956..4f949a2c8c 100644 --- a/packages/auth/amplify_auth_cognito_test/test/plugin/auth_providers_test.dart +++ b/packages/auth/amplify_auth_cognito_test/test/plugin/auth_providers_test.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart' hide InternalErrorException; import 'package:amplify_auth_cognito_dart/src/credentials/cognito_keys.dart'; +import 'package:amplify_auth_cognito_dart/src/model/auth_result.dart'; import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart'; import 'package:amplify_auth_cognito_dart/src/util/cognito_user_pools_auth_provider.dart'; import 'package:amplify_core/amplify_core.dart'; @@ -27,20 +28,25 @@ class TestAmplifyAuthUserPoolOnly extends AmplifyAuthCognitoDart { Future fetchAuthSession({ CognitoSessionOptions? options, }) async { - final getAWSCredentials = options?.getAWSCredentials; - if (getAWSCredentials != null && getAWSCredentials) { - throw const InvalidAccountTypeException.noIdentityPool( - recoverySuggestion: - 'Register an identity pool using the CLI or set getAWSCredentials ' - 'to false', - ); - } return CognitoAuthSession( isSignedIn: true, - userPoolTokens: CognitoUserPoolTokens( - accessToken: accessToken, - idToken: idToken, - refreshToken: refreshToken, + userPoolTokensResult: AuthResult.success( + CognitoUserPoolTokens( + accessToken: accessToken, + idToken: idToken, + refreshToken: refreshToken, + ), + ), + userSubResult: const AuthResult.success(userSub), + credentialsResult: const AuthResult.error( + InvalidAccountTypeException.noIdentityPool( + recoverySuggestion: 'Register an identity pool using the CLI', + ), + ), + identityIdResult: const AuthResult.error( + InvalidAccountTypeException.noIdentityPool( + recoverySuggestion: 'Register an identity pool using the CLI', + ), ), ); } diff --git a/packages/auth/amplify_auth_cognito_test/test/state/fetch_auth_session_state_machine_test.dart b/packages/auth/amplify_auth_cognito_test/test/state/fetch_auth_session_state_machine_test.dart index 046eba9b3e..e1a7b971eb 100644 --- a/packages/auth/amplify_auth_cognito_test/test/state/fetch_auth_session_state_machine_test.dart +++ b/packages/auth/amplify_auth_cognito_test/test/state/fetch_auth_session_state_machine_test.dart @@ -3,10 +3,10 @@ import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart'; import 'package:amplify_auth_cognito_dart/src/credentials/cognito_keys.dart'; -import 'package:amplify_auth_cognito_dart/src/model/auth_configuration.dart'; import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity.dart'; import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart'; import 'package:amplify_auth_cognito_dart/src/state/state.dart'; +import 'package:amplify_core/amplify_core.dart'; import 'package:amplify_secure_storage_dart/amplify_secure_storage_dart.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:test/test.dart'; @@ -20,6 +20,61 @@ void main() { group('FetchAuthSessionStateMachine', () { late CognitoAuthStateMachine stateMachine; late SecureStorageInterface secureStorage; + late AmplifyConfig config; + late CognitoAuthSession session; + + final newAccessToken = createJwt( + type: TokenType.id, + expiration: const Duration(minutes: 5), + ); + + final expiredAccessToken = createJwt( + type: TokenType.access, + expiration: Duration.zero, + ); + + final newIdToken = createJwt( + type: TokenType.id, + expiration: const Duration(minutes: 5), + ); + + final expiredIdToken = createJwt( + type: TokenType.id, + expiration: Duration.zero, + ); + + const newAccessKeyId = 'newAccessKeyId'; + const newSecretAccessKey = 'newSecretAccessKey'; + + Future configureAmplify(AmplifyConfig config) async { + stateMachine.dispatch(AuthEvent.configure(config)); + await stateMachine.stream.whereType().first; + } + + Future fetchAuthSession({ + bool forceRefresh = false, + required bool willRefresh, + }) async { + stateMachine.dispatch( + FetchAuthSessionEvent.fetch( + CognitoSessionOptions(forceRefresh: forceRefresh), + ), + ); + final sm = stateMachine.getOrCreate( + FetchAuthSessionStateMachine.type, + ); + await expectLater( + sm.stream.startWith(sm.currentState), + emitsInOrder([ + isA(), + isA(), + if (willRefresh) isA(), + isA(), + ]), + ); + final state = sm.currentState as FetchAuthSessionSuccess; + return state.session; + } setUp(() { secureStorage = MockSecureStorage(); @@ -29,658 +84,1202 @@ void main() { ..addInstance(authConfig); }); - group('fetch', () { - test('(isSignedIn=false)', () async { - stateMachine - ..dispatch(const CredentialStoreEvent.migrateLegacyCredentialStore()) - ..dispatch(const FetchAuthSessionEvent.fetch()); - - final sm = stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - ]), - ); - - final state = sm.currentState as FetchAuthSessionSuccess; - final session = state.session; - expect(session.isSignedIn, isFalse); + group('User Pool + Identity Pool', () { + setUp(() { + config = mockConfig; }); - - test('(isSignedIn=true, getAwsCredentials=false)', () async { - seedStorage( - secureStorage, - userPoolKeys: userPoolKeys, - ); - stateMachine - ..dispatch(const CredentialStoreEvent.migrateLegacyCredentialStore()) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: false), - ), - ); - - final sm = stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - ]), - ); - - final state = sm.currentState as FetchAuthSessionSuccess; - final session = state.session; - expect(session.isSignedIn, isTrue); - expect(session.identityId, isNull); - expect(session.credentials, isNull); - - expect(sm.getLatestResult(), completion(state)); - }); - - test('(isSignedIn=true, getAwsCredentials=true)', () async { - seedStorage( - secureStorage, - userPoolKeys: userPoolKeys, - ); - stateMachine.dispatch(AuthEvent.configure(mockConfig)); - await stateMachine.stream.whereType().first; - - stateMachine - ..addInstance( - MockCognitoIdentityClient( - getId: () async => GetIdResponse(identityId: identityId), - getCredentialsForIdentity: () async => - GetCredentialsForIdentityResponse( - credentials: Credentials( - accessKeyId: accessKeyId, - secretKey: secretAccessKey, - ), - ), - ), - ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: true), - ), + group('credentials & tokens valid', () { + setUp(() { + seedStorage( + secureStorage, + identityPoolKeys: identityPoolKeys, + userPoolKeys: userPoolKeys, ); + }); - final sm = stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - isA(), - ]), - ); - - final state = sm.currentState as FetchAuthSessionSuccess; - final session = state.session; - expect(session.isSignedIn, isTrue); - expect(session.identityId, identityId); - expect(session.credentials, isNotNull); - expect(session.credentials!.accessKeyId, accessKeyId); - expect(session.credentials!.secretAccessKey, secretAccessKey); - expect(session.credentials!.sessionToken, isNull); - expect(session.credentials!.expiration, isNull); - - expect(sm.getLatestResult(), completion(state)); - }); + group('fetch', () { + late CognitoAuthSession session; + setUp(() async { + await configureAmplify(config); + session = await fetchAuthSession(willRefresh: false); + }); - group('user pool-only', () { - setUp(() { - stateMachine - ..addInstance(userPoolOnlyConfig) - ..addInstance( - AuthConfiguration.fromConfig( - userPoolOnlyConfig.auth!.awsPlugin!, - ), - ) - ..dispatch( - const CredentialStoreEvent.migrateLegacyCredentialStore(), - ); - }); + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); - test('succeeds for user pool only requests', () { - stateMachine.dispatch( - const FetchAuthSessionEvent.fetch(), - ); + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); - expect( - stateMachine - .expect(FetchAuthSessionStateMachine.type) - .getLatestResult(), - completes, - ); - }); + test('should return existing user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, accessToken); + expect(userPoolTokens.idToken, idToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); - test('throws when aws creds are requested', () { - stateMachine.dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: true), - ), - ); + test('should return existing credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, accessKeyId); + expect(credentials.secretAccessKey, secretAccessKey); + }); - expect( - stateMachine - .expect(FetchAuthSessionStateMachine.type) - .getLatestResult(), - throwsA(isA()), - ); + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); }); }); - group('refresh', () { - test('AWS creds (success)', () async { + group('expired credentials', () { + setUp(() { seedStorage( secureStorage, identityPoolKeys: identityPoolKeys, + userPoolKeys: userPoolKeys, ); secureStorage.write( key: identityPoolKeys[CognitoIdentityPoolKey.expiration], value: DateTime.now().toIso8601String(), ); - stateMachine.dispatch(AuthEvent.configure(mockConfig)); - await stateMachine.stream.whereType().first; - - const newAccessKeyId = 'newAccessKeyId'; - const newSecretAccessKey = 'newSecretAccessKey'; - stateMachine - ..addInstance( - MockCognitoIdentityClient( - getCredentialsForIdentity: expectAsync0( - () async => GetCredentialsForIdentityResponse( - credentials: Credentials( - accessKeyId: newAccessKeyId, - secretKey: newSecretAccessKey, + }); + + group('fetch', () { + group('success', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityClient( + getCredentialsForIdentity: expectAsync0( + () async => GetCredentialsForIdentityResponse( + credentials: Credentials( + accessKeyId: newAccessKeyId, + secretKey: newSecretAccessKey, + ), ), ), ), - ), - ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: true), - ), + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return existing user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, accessToken); + expect(userPoolTokens.idToken, idToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should return new credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, newAccessKeyId); + expect(credentials.secretAccessKey, newSecretAccessKey); + }); + + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); + + group('network error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityClient( + getCredentialsForIdentity: expectAsync0( + () async => throw AWSHttpException( + AWSHttpRequest.get(Uri()), + ), + ), + ), + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return existing user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, accessToken); + expect(userPoolTokens.idToken, idToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test( + 'should throw a NetworkException when accessing credentials', + () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }, ); - final sm = - stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - isA(), - ]), - ); + test( + 'should throw a NetworkException when accessing identityId', + () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }, + ); + }); - final state = sm.currentState as FetchAuthSessionSuccess; - expect( - state.session.identityId, - identityId, - reason: 'Should retain identity ID', - ); - expect( - state.session.credentials?.accessKeyId, - newAccessKeyId, - ); - expect( - state.session.credentials?.secretAccessKey, - newSecretAccessKey, - ); - expect(state.session.credentials?.sessionToken, isNull); - expect(state.session.credentials?.expiration, isNull); + group('unknown error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityClient( + getCredentialsForIdentity: expectAsync0( + () async => throw _ServiceException(), + ), + ), + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return existing user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, accessToken); + expect(userPoolTokens.idToken, idToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); }); + }); - test('AWS creds (failure)', () async { + group('expired ID Token', () { + setUp(() { seedStorage( secureStorage, identityPoolKeys: identityPoolKeys, + userPoolKeys: userPoolKeys, ); secureStorage.write( - key: identityPoolKeys[CognitoIdentityPoolKey.expiration], - value: DateTime.now().toIso8601String(), + key: userPoolKeys[CognitoUserPoolKey.idToken], + value: expiredIdToken.raw, ); - stateMachine.dispatch(AuthEvent.configure(mockConfig)); - await stateMachine.stream.whereType().first; - - stateMachine - ..addInstance( - MockCognitoIdentityClient( - getCredentialsForIdentity: expectAsync0( - () async => throw _FetchAuthSessionException(), + }); + + group('fetch', () { + group('success', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => InitiateAuthResponse( + authenticationResult: AuthenticationResultType( + accessToken: newAccessToken.raw, + refreshToken: refreshToken, + idToken: newIdToken.raw, + ), + ), + ), ), - ), - ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: true), - ), - ); + ); + session = await fetchAuthSession(willRefresh: true); + }); - final sm = - stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - isA(), - ]), - ); + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return new user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, newAccessToken); + expect(userPoolTokens.idToken, newIdToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should return existing credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, accessKeyId); + expect(credentials.secretAccessKey, secretAccessKey); + }); + + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); - final state = sm.currentState as FetchAuthSessionFailure; - expect(state.exception, isA<_FetchAuthSessionException>()); - expect( - sm.getLatestResult(), - throwsA(isA<_FetchAuthSessionException>()), + group('network error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw AWSHttpException( + AWSHttpRequest.get(Uri()), + ), + ), + ), + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should return existing credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, accessKeyId); + expect(credentials.secretAccessKey, secretAccessKey); + }); + + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); + + group('unknown error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw _ServiceException(), + ), + ), + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should return existing credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, accessKeyId); + expect(credentials.secretAccessKey, secretAccessKey); + }); + + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); + }); + }); + + group('expired Access Token', () { + setUp(() { + seedStorage( + secureStorage, + identityPoolKeys: identityPoolKeys, + userPoolKeys: userPoolKeys, + ); + secureStorage.write( + key: userPoolKeys[CognitoUserPoolKey.accessToken], + value: expiredAccessToken.raw, ); }); - group('User Pool tokens (success)', () { - Future runTest({bool willRefresh = true}) async { - stateMachine.dispatch(AuthEvent.configure(mockConfig)); - await stateMachine.stream.whereType().first; + group('fetch', () { + group('success', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => InitiateAuthResponse( + authenticationResult: AuthenticationResultType( + accessToken: newAccessToken.raw, + refreshToken: refreshToken, + idToken: newIdToken.raw, + ), + ), + ), + ), + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return new user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, newAccessToken); + expect(userPoolTokens.idToken, newIdToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should return existing credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, accessKeyId); + expect(credentials.secretAccessKey, secretAccessKey); + }); + + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); + group('network error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw AWSHttpException( + AWSHttpRequest.get(Uri()), + ), + ), + ), + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should return existing credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, accessKeyId); + expect(credentials.secretAccessKey, secretAccessKey); + }); + + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); + + group('unknown error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw _ServiceException(), + ), + ), + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + test('should return existing credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, accessKeyId); + expect(credentials.secretAccessKey, secretAccessKey); + }); + + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); + }); + }); + + group('force refresh', () { + setUp(() { + seedStorage( + secureStorage, + identityPoolKeys: identityPoolKeys, + userPoolKeys: userPoolKeys, + ); + }); + group('success', () { + setUp(() async { + await configureAmplify(config); stateMachine ..addInstance( MockCognitoIdentityProviderClient( initiateAuth: expectAsync0( - count: willRefresh ? 1 : 0, () async => InitiateAuthResponse( authenticationResult: AuthenticationResultType( - accessToken: accessToken.raw, + accessToken: newAccessToken.raw, refreshToken: refreshToken, - idToken: idToken.raw, + idToken: newIdToken.raw, ), ), ), ), ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: false), + ..addInstance( + MockCognitoIdentityClient( + getCredentialsForIdentity: expectAsync0( + () async => GetCredentialsForIdentityResponse( + credentials: Credentials( + accessKeyId: newAccessKeyId, + secretKey: newSecretAccessKey, + ), + ), + ), ), ); + session = await fetchAuthSession( + willRefresh: true, + forceRefresh: true, + ); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); - final sm = - stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - if (willRefresh) isA(), - isA(), - ]), + test('should return new user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, newAccessToken); + expect(userPoolTokens.idToken, newIdToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should return new credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, newAccessKeyId); + expect(credentials.secretAccessKey, newSecretAccessKey); + }); + + test('should return existing identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); + + group('network error', () { + setUp(() async { + await configureAmplify(config); + stateMachine + ..addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw AWSHttpException( + AWSHttpRequest.get(Uri()), + ), + ), + ), + ) + ..addInstance( + MockCognitoIdentityClient( + getCredentialsForIdentity: expectAsync0( + () async => throw AWSHttpException( + AWSHttpRequest.get(Uri()), + ), + ), + ), + ); + session = await fetchAuthSession( + willRefresh: true, + forceRefresh: true, ); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); - final state = sm.currentState as FetchAuthSessionSuccess; - expect(state.session.isSignedIn, isTrue); - expect(state.session.userSub, userSub); - expect(state.session.userPoolTokens, isNotNull); - expect(state.session.userPoolTokens?.accessToken, accessToken); - expect(state.session.userPoolTokens?.refreshToken, refreshToken); - expect(state.session.userPoolTokens?.idToken, idToken); - } - - test('access token expires', () async { - seedStorage( - secureStorage, - userPoolKeys: userPoolKeys, + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), ); - secureStorage.write( - key: userPoolKeys[CognitoUserPoolKey.accessToken], - value: createJwt( - type: TokenType.access, - expiration: Duration.zero, - ).raw, + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), ); - await runTest(); }); - test('id token expires', () async { - seedStorage( - secureStorage, - userPoolKeys: userPoolKeys, + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), ); - secureStorage.write( - key: userPoolKeys[CognitoUserPoolKey.idToken], - value: createJwt( - type: TokenType.id, - expiration: Duration.zero, - ).raw, + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), ); - await runTest(); }); + }); - test('neither token expires', () async { - seedStorage( - secureStorage, - userPoolKeys: userPoolKeys, + group('unknown error', () { + setUp(() async { + await configureAmplify(config); + stateMachine + ..addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw _ServiceException(), + ), + ), + ) + ..addInstance( + MockCognitoIdentityClient( + getCredentialsForIdentity: expectAsync0( + () async => throw _ServiceException(), + ), + ), + ); + session = await fetchAuthSession( + willRefresh: true, + forceRefresh: true, + ); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), ); - await runTest(willRefresh: false); }); }); + }); - test('User Pool tokens (failure)', () async { - seedStorage( - secureStorage, - userPoolKeys: userPoolKeys, - ); - secureStorage.write( - key: userPoolKeys[CognitoUserPoolKey.accessToken], - value: createJwt( - type: TokenType.access, - expiration: Duration.zero, - ).raw, - ); - stateMachine.dispatch(AuthEvent.configure(mockConfig)); - await stateMachine.stream.whereType().first; + group('no user pool tokens (not signed in)', () { + group('unauthorized access supported', () { + group('fetch', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityClient( + getId: expectAsync0( + () async => GetIdResponse(identityId: identityId), + ), + getCredentialsForIdentity: expectAsync0( + () async => GetCredentialsForIdentityResponse( + credentials: Credentials( + accessKeyId: newAccessKeyId, + secretKey: newSecretAccessKey, + ), + ), + ), + ), + ); + session = await fetchAuthSession(willRefresh: true); + }); - stateMachine - ..addInstance( - MockCognitoIdentityProviderClient( - initiateAuth: expectAsync0( - () async => throw _FetchAuthSessionException(), + test('should return isSignedIn=false', () { + expect(session.isSignedIn, isFalse); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should return new credentials', () { + final credentials = session.credentialsResult.value; + expect(credentials.accessKeyId, newAccessKeyId); + expect(credentials.secretAccessKey, newSecretAccessKey); + }); + + test('should return identityId', () { + expect(session.identityIdResult.value, identityId); + }); + }); + }); + + group('unauthorized access not supported', () { + group('fetch', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityClient( + getId: expectAsync0( + () async => throw const AuthNotAuthorizedException( + 'Not Authorized', + ), + ), ), - ), - ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: false), - ), - ); + ); - final sm = - stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - isA(), - ]), - ); + session = await fetchAuthSession(willRefresh: true); + }); - final state = sm.currentState as FetchAuthSessionFailure; - expect(state.exception, isA<_FetchAuthSessionException>()); - expect( - sm.getLatestResult(), - throwsA(isA<_FetchAuthSessionException>()), - ); + test('should return isSignedIn=false', () { + expect(session.isSignedIn, isFalse); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); + }); + }); + }); + group('User Pool Only Config', () { + setUp(() { + config = userPoolOnlyConfig; + }); + group('tokens valid', () { + setUp(() { + seedStorage(secureStorage, userPoolKeys: userPoolKeys); }); - test('force refresh user pool tokens', () async { - seedStorage( - secureStorage, - userPoolKeys: userPoolKeys, - ); - // Create an unexpired access token which we want to refresh. - final originalToken = createJwt( - type: TokenType.access, - expiration: const Duration(minutes: 3), - ); + group('fetch', () { + setUp(() async { + await configureAmplify(config); + session = await fetchAuthSession(willRefresh: false); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return existing user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, accessToken); + expect(userPoolTokens.idToken, idToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); + }); + + group('expired ID Token', () { + setUp(() { + seedStorage(secureStorage, userPoolKeys: userPoolKeys); secureStorage.write( - key: userPoolKeys[CognitoUserPoolKey.accessToken], - value: originalToken.raw, + key: userPoolKeys[CognitoUserPoolKey.idToken], + value: expiredIdToken.raw, ); - stateMachine.dispatch(AuthEvent.configure(userPoolOnlyConfig)); - await stateMachine.stream.whereType().first; + }); - stateMachine - ..addInstance( - MockCognitoIdentityProviderClient( - initiateAuth: expectAsync0( - () async => InitiateAuthResponse( - authenticationResult: AuthenticationResultType( - accessToken: createJwt( - type: TokenType.access, - expiration: const Duration(minutes: 5), - ).raw, + group('fetch', () { + group('success', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => InitiateAuthResponse( + authenticationResult: AuthenticationResultType( + accessToken: newAccessToken.raw, + refreshToken: refreshToken, + idToken: newIdToken.raw, + ), ), ), ), - ), - ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions( - getAWSCredentials: false, - forceRefresh: true, + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return new user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, newAccessToken); + expect(userPoolTokens.idToken, newIdToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); + group('unknown error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw _ServiceException(), + ), ), - ), - ); + ); + session = await fetchAuthSession(willRefresh: true); + }); + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); - final sm = - stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - isA(), - ]), - ); + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); - final state = sm.currentState as FetchAuthSessionSuccess; - expect(state.session.isSignedIn, isTrue); - expect(state.session.userSub, userSub); - expect(state.session.userPoolTokens, isNotNull); - - final newToken = state.session.userPoolTokens!.accessToken; - expect(newToken, isNot(originalToken)); - expect( - newToken.claims.expiration!.millisecondsSinceEpoch, - greaterThan( - originalToken.claims.expiration!.millisecondsSinceEpoch, - ), - ); + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); }); + }); - test('force refresh AWS creds', () async { - seedStorage( - secureStorage, - identityPoolKeys: identityPoolKeys, - ); - // Create unexpired AWS credentials which we want to refresh. - final originalExpiration = - DateTime.now().add(const Duration(minutes: 3)); - final newExpiration = DateTime.now().add(const Duration(minutes: 5)); + group('expired Access Token', () { + setUp(() { + seedStorage(secureStorage, userPoolKeys: userPoolKeys); secureStorage.write( - key: identityPoolKeys[CognitoIdentityPoolKey.expiration], - value: originalExpiration.toIso8601String(), + key: userPoolKeys[CognitoUserPoolKey.accessToken], + value: expiredAccessToken.raw, ); - stateMachine.dispatch(AuthEvent.configure(mockConfig)); - await stateMachine.stream.whereType().first; - - const newAccessKeyId = 'newAccessKeyId'; - const newSecretAccessKey = 'newSecretAccessKey'; - stateMachine - ..addInstance( - MockCognitoIdentityClient( - getCredentialsForIdentity: expectAsync0( - () async => GetCredentialsForIdentityResponse( - credentials: Credentials( - accessKeyId: newAccessKeyId, - secretKey: newSecretAccessKey, - expiration: newExpiration, + }); + + group('fetch', () { + group('success', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => InitiateAuthResponse( + authenticationResult: AuthenticationResultType( + accessToken: newAccessToken.raw, + refreshToken: refreshToken, + idToken: newIdToken.raw, + ), ), ), ), - ), - ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions( - getAWSCredentials: true, - forceRefresh: true, + ); + session = await fetchAuthSession(willRefresh: true); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return new user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, newAccessToken); + expect(userPoolTokens.idToken, newIdToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); + group('unknown error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw _ServiceException(), + ), ), - ), - ); + ); + session = await fetchAuthSession(willRefresh: true); + }); + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); - final sm = - stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - isA(), - ]), - ); + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); - final state = sm.currentState as FetchAuthSessionSuccess; - expect( - state.session.identityId, - identityId, - reason: 'Should retain identity ID', - ); - expect( - state.session.credentials?.accessKeyId, - newAccessKeyId, - ); - expect( - state.session.credentials?.secretAccessKey, - newSecretAccessKey, - ); - expect(state.session.credentials?.expiration, newExpiration); + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); }); + }); - test('force refresh all creds', () async { + group('force refresh', () { + setUp(() { seedStorage( secureStorage, identityPoolKeys: identityPoolKeys, userPoolKeys: userPoolKeys, ); - // Create an unexpired access token which we want to refresh. - final originalToken = createJwt( - type: TokenType.access, - expiration: const Duration(minutes: 3), - ); - secureStorage.write( - key: userPoolKeys[CognitoUserPoolKey.accessToken], - value: originalToken.raw, - ); - // Create unexpired AWS credentials which we don't want to refresh. - final originalExpiration = - DateTime.now().add(const Duration(minutes: 3)); - secureStorage.write( - key: identityPoolKeys[CognitoIdentityPoolKey.expiration], - value: originalExpiration.toIso8601String(), - ); - stateMachine.dispatch(AuthEvent.configure(mockConfig)); - await stateMachine.stream.whereType().first; - - stateMachine - ..addInstance( + }); + group('success', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( MockCognitoIdentityProviderClient( initiateAuth: expectAsync0( () async => InitiateAuthResponse( authenticationResult: AuthenticationResultType( - accessToken: createJwt( - type: TokenType.access, - expiration: const Duration(minutes: 5), - ).raw, + accessToken: newAccessToken.raw, + refreshToken: refreshToken, + idToken: newIdToken.raw, ), ), ), ), - ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions( - getAWSCredentials: false, - forceRefresh: true, + ); + session = await fetchAuthSession( + willRefresh: true, + forceRefresh: true, + ); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should return existing user sub', () { + expect(session.userSubResult.value, userSub); + }); + + test('should return new user pool tokens', () { + final userPoolTokens = session.userPoolTokensResult.value; + expect(userPoolTokens.accessToken, newAccessToken); + expect(userPoolTokens.idToken, newIdToken); + expect(userPoolTokens.refreshToken, refreshToken); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); + + group('unknown error', () { + setUp(() async { + await configureAmplify(config); + stateMachine.addInstance( + MockCognitoIdentityProviderClient( + initiateAuth: expectAsync0( + () async => throw _ServiceException(), ), ), ); + session = await fetchAuthSession( + willRefresh: true, + forceRefresh: true, + ); + }); + + test('should return isSignedIn=true', () { + expect(session.isSignedIn, isTrue); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); - final sm = - stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - isA(), - ]), - ); + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); - final state = sm.currentState as FetchAuthSessionSuccess; - expect(state.session.isSignedIn, isTrue); - expect(state.session.userSub, userSub); - expect(state.session.userPoolTokens, isNotNull); - - final newToken = state.session.userPoolTokens!.accessToken; - expect(newToken, isNot(originalToken)); - expect( - newToken.claims.expiration!.millisecondsSinceEpoch, - greaterThan( - originalToken.claims.expiration!.millisecondsSinceEpoch, - ), - ); + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); - expect(state.session.credentials, isNotNull); - expect(state.session.credentials!.expiration, originalExpiration); + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); }); }); - test('fails', () async { - seedStorage( - secureStorage, - userPoolKeys: userPoolKeys, - ); - stateMachine.dispatch(AuthEvent.configure(mockConfig)); - await stateMachine.stream.whereType().first; - - stateMachine - ..addInstance( - MockCognitoIdentityClient( - getId: () async => throw _FetchAuthSessionException(), - getCredentialsForIdentity: () async => - throw _FetchAuthSessionException(), - ), - ) - ..dispatch( - const FetchAuthSessionEvent.fetch( - CognitoSessionOptions(getAWSCredentials: true), - ), - ); + group('no user pool tokens (not signed in)', () { + group('fetch', () { + setUp(() async { + await configureAmplify(config); + session = await fetchAuthSession(willRefresh: false); + }); - final sm = stateMachine.getOrCreate(FetchAuthSessionStateMachine.type); - await expectLater( - sm.stream.startWith(sm.currentState), - emitsInOrder([ - isA(), - isA(), - isA(), - isA(), - ]), - ); - - final state = sm.currentState as FetchAuthSessionFailure; - expect(state.exception, isA<_FetchAuthSessionException>()); - expect( - sm.getLatestResult(), - throwsA(isA<_FetchAuthSessionException>()), - ); + test('should return isSignedIn=false', () { + expect(session.isSignedIn, isFalse); + }); + + test('should throw when accessing user sub', () { + expect( + () => session.userSubResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing user pool tokens', () { + expect( + () => session.userPoolTokensResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing credentials', () { + expect( + () => session.credentialsResult.value, + throwsA(isA()), + ); + }); + + test('should throw when accessing identityId', () { + expect( + () => session.identityIdResult.value, + throwsA(isA()), + ); + }); + }); }); }); }); } -class _FetchAuthSessionException implements Exception {} +/// A mock exception thrown by a service. +class _ServiceException implements Exception {} diff --git a/packages/authenticator/amplify_authenticator/lib/src/services/amplify_auth_service.dart b/packages/authenticator/amplify_authenticator/lib/src/services/amplify_auth_service.dart index 8b2fcb8734..10dc30f3ca 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/services/amplify_auth_service.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/services/amplify_auth_service.dart @@ -162,12 +162,20 @@ class AmplifyAuthService implements AuthService { @override Future isValidSession() async { + final res = await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; try { - final res = await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; - return res.userPoolTokens != null; + // If tokens can be retrieved without an exception, return true. + res.userPoolTokensResult.value; + return true; } on SignedOutException { return false; + } on NetworkException { + // NetworkException indicates that access and/or id tokens have expired + // and cannot be refreshed due to a network error. In this case the user + // should be treated as authenticated to allow for offline use cases. + return true; } on Exception { + // Any other exception should be thrown to be handled appropriately. rethrow; } } diff --git a/packages/aws_common/lib/aws_common.dart b/packages/aws_common/lib/aws_common.dart index 8bb640bc5d..4b10651392 100644 --- a/packages/aws_common/lib/aws_common.dart +++ b/packages/aws_common/lib/aws_common.dart @@ -41,6 +41,9 @@ export 'src/logging/simple_log_printer.dart'; // Operation export 'src/operation/aws_operation.dart'; export 'src/operation/aws_progress_operation.dart'; +// Result +export 'src/result/aws_result.dart'; +export 'src/result/aws_result_type.dart'; // Utils export 'src/util/cancelable.dart'; export 'src/util/closeable.dart'; diff --git a/packages/aws_common/lib/src/result/aws_result.dart b/packages/aws_common/lib/src/result/aws_result.dart new file mode 100644 index 0000000000..6359b3140d --- /dev/null +++ b/packages/aws_common/lib/src/result/aws_result.dart @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:aws_common/aws_common.dart'; + +/// {@template aws_common.aws_result} +/// The result of an operation that may throw an exception. +/// +/// [value] will return the result if it was retrieved successfully, or +/// throw an exception if an exception occurred. See [exception] for more +/// details. +/// {@endtemplate} +class AWSResult + with AWSEquatable>, AWSDebuggable { + /// Creates a failed result. + const AWSResult.error(E this.exception, [this.stackTrace]) + : type = AWSResultType.error, + _value = null; + + /// Creates a successful result. + const AWSResult.success(T value) + : _value = value, + type = AWSResultType.success, + exception = null, + stackTrace = null; + + /// The value of the result, or null. + final T? _value; + + /// The exception that occurred while attempting to retrieve the value. + final E? exception; + + /// The original stack trace of [exception], if provided. + final StackTrace? stackTrace; + + /// Indicates if the result was a success. + final AWSResultType type; + + /// The value of the result, if the result was successful. + /// + /// If an exception was thrown while retrieving the value, this will throw. + /// See [exception] for more details. + T get value { + switch (type) { + case AWSResultType.success: + // value will be non-null since it is required in AWSResult.success. + return _value!; + case AWSResultType.error: + if (stackTrace != null) { + // TODO(dnys1): Chain, instead, so that the current stack trace can + /// provide context to the original + } + // ignore: only_throw_errors + throw exception!; + } + } + + /// The value of the result, or null if there was an error retrieving it. + T? get valueOrNull { + switch (type) { + case AWSResultType.success: + // value will be non-null since it is required in AWSResult.success. + return _value!; + case AWSResultType.error: + return null; + } + } + + @override + List get props => [ + _value, + exception, + type, + ]; + + @override + String get runtimeTypeName => 'AWSResult'; +} diff --git a/packages/aws_common/lib/src/result/aws_result_type.dart b/packages/aws_common/lib/src/result/aws_result_type.dart new file mode 100644 index 0000000000..24246c63ce --- /dev/null +++ b/packages/aws_common/lib/src/result/aws_result_type.dart @@ -0,0 +1,11 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/// Indicates whether or not the result was successful. +enum AWSResultType { + /// A successful result. + success, + + /// An unsuccessful result. + error, +} diff --git a/packages/storage/amplify_storage_s3/example/integration_test/main_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/main_test.dart index a0192a9074..08213c4cca 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/main_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/main_test.dart @@ -26,12 +26,8 @@ class CustomPrefixResolver implements S3PrefixResolver { required StorageAccessLevel accessLevel, String? identityId, }) async { - final currentUserIdentityId = ((await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions( - getAWSCredentials: true, - ), - )) as CognitoAuthSession) - .identityId; + final session = await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; + final currentUserIdentityId = session.identityIdResult.value; switch (accessLevel) { case StorageAccessLevel.guest: return 'everyone/'; @@ -116,12 +112,9 @@ void main() async { username: username1, password: password, ); - final user1Session = await Amplify.Auth.fetchAuthSession( - options: const CognitoSessionOptions( - getAWSCredentials: true, - ), - ); - user1IdentityId = (user1Session as CognitoAuthSession).identityId!; + final user1Session = await Amplify.Auth.fetchAuthSession(); + user1IdentityId = + (user1Session as CognitoAuthSession).identityIdResult.value; await Amplify.Auth.signOut(); await Amplify.Auth.signIn(