diff --git a/packages/amplify_core/doc/lib/auth.dart b/packages/amplify_core/doc/lib/auth.dart index a69fde0f6c..6e48014a9b 100644 --- a/packages/amplify_core/doc/lib/auth.dart +++ b/packages/amplify_core/doc/lib/auth.dart @@ -483,6 +483,17 @@ Future forgetSpecificDevice(AuthDevice myDevice) async { } // #enddocregion forget-specific-device +// #docregion fetch-current-device +Future fetchCurrentDevice() async { + try { + final device = await Amplify.Auth.fetchCurrentDevice(); + safePrint('Device: $device'); + } on AuthException catch (e) { + safePrint('Fetch current device failed with error: $e'); + } +} +// #enddocregion fetch-current-device + // #docregion fetch-devices Future fetchAllDevices() async { try { diff --git a/packages/amplify_core/lib/src/category/amplify_auth_category.dart b/packages/amplify_core/lib/src/category/amplify_auth_category.dart index 4a23528d70..bf3bfd27ff 100644 --- a/packages/amplify_core/lib/src/category/amplify_auth_category.dart +++ b/packages/amplify_core/lib/src/category/amplify_auth_category.dart @@ -1355,6 +1355,37 @@ class AuthCategory extends AmplifyCategory { () => defaultPlugin.rememberDevice(), ); + /// {@template amplify_core.amplify_auth_category.fetch_current_device} + /// Retrieves the current device. + /// + /// For more information about device tracking, see the + /// [Amplify docs](https://docs.amplify.aws/flutter/build-a-backend/auth/manage-users/manage-devices/#fetch-current-device). + /// + /// ## Examples + /// + /// + /// ```dart + /// import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; + /// import 'package:amplify_flutter/amplify_flutter.dart'; + /// ``` + /// + /// + /// ```dart + /// Future getCurrentUserDevice() async { + /// try { + /// final device = await Amplify.Auth.fetchCurrentDevice(); + /// safePrint('Device: $device'); + /// } on AuthException catch (e) { + /// safePrint('Fetch current device failed with error: $e'); + /// } + /// } + /// ``` + /// {@endtemplate} + Future fetchCurrentDevice() => identifyCall( + AuthCategoryMethod.fetchCurrentDevice, + () => defaultPlugin.fetchCurrentDevice(), + ); + /// {@template amplify_core.amplify_auth_category.forget_device} /// Forgets the current device. /// diff --git a/packages/amplify_core/lib/src/http/amplify_category_method.dart b/packages/amplify_core/lib/src/http/amplify_category_method.dart index a0c978594b..97ee62251e 100644 --- a/packages/amplify_core/lib/src/http/amplify_category_method.dart +++ b/packages/amplify_core/lib/src/http/amplify_category_method.dart @@ -52,7 +52,8 @@ enum AuthCategoryMethod with AmplifyCategoryMethod { setMfaPreference('49'), getMfaPreference('50'), setUpTotp('51'), - verifyTotpSetup('52'); + verifyTotpSetup('52'), + fetchCurrentDevice('59'); const AuthCategoryMethod(this.method); diff --git a/packages/amplify_core/lib/src/plugin/amplify_auth_plugin_interface.dart b/packages/amplify_core/lib/src/plugin/amplify_auth_plugin_interface.dart index 43e74853f3..80521de18d 100644 --- a/packages/amplify_core/lib/src/plugin/amplify_auth_plugin_interface.dart +++ b/packages/amplify_core/lib/src/plugin/amplify_auth_plugin_interface.dart @@ -189,6 +189,11 @@ abstract class AuthPluginInterface extends AmplifyPluginInterface { throw UnimplementedError('forgetDevice() has not been implemented.'); } + /// {@macro amplify_core.amplify_auth_category.fetch_current_device} + Future fetchCurrentDevice() { + throw UnimplementedError('fetchCurrentDevice() has not been implemented.'); + } + /// {@macro amplify_core.amplify_auth_category.fetch_devices} Future> fetchDevices() { throw UnimplementedError('fetchDevices() has not been implemented.'); diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/device_tracking_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/device_tracking_test.dart index 500582ad0a..c877ffca52 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/device_tracking_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/device_tracking_test.dart @@ -240,6 +240,45 @@ void main() { await expectLater(Amplify.Auth.rememberDevice(), completes); }); + asyncTest('fetchCurrentDevice returns the current device', (_) async { + await expectLater(Amplify.Auth.fetchCurrentDevice(), completes); + final currentTestDevice = await Amplify.Auth.fetchCurrentDevice(); + final currentDeviceKey = await getDeviceKey(); + expect(currentDeviceKey, currentTestDevice.id); + }); + + asyncTest( + 'The device from fetchCurrentDevice isnt equal to another device.', + (_) async { + final previousDeviceKey = await getDeviceKey(); + await signOutUser(); + await deleteDevice(cognitoUsername, previousDeviceKey!); + await signIn(); + final newCurrentTestDevice = await Amplify.Auth.fetchCurrentDevice(); + expect(newCurrentTestDevice.id, isNot(previousDeviceKey)); + }); + + asyncTest( + 'fetchCurrentDevice throws a DeviceNotTrackedException when device is forgotten.', + (_) async { + expect(await getDeviceState(), DeviceState.remembered); + await Amplify.Auth.forgetDevice(); + await expectLater( + Amplify.Auth.fetchCurrentDevice, + throwsA(isA()), + ); + }); + + asyncTest( + 'fetchCurrentDevice throws a SignedOutException when device signs out.', + (_) async { + await signOutUser(); + await expectLater( + Amplify.Auth.fetchCurrentDevice, + throwsA(isA()), + ); + }); + asyncTest('forgetDevice stops tracking', (_) async { expect(await getDeviceState(), DeviceState.remembered); await Amplify.Auth.forgetDevice(); 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 42347c4c90..b266f9ee7d 100644 --- a/packages/auth/amplify_auth_cognito_dart/example/lib/common.dart +++ b/packages/auth/amplify_auth_cognito_dart/example/lib/common.dart @@ -104,6 +104,10 @@ Future> fetchUserAttributes() async { return Amplify.Auth.fetchUserAttributes(); } +Future fetchCurrentDevice() async { + return Amplify.Auth.fetchCurrentDevice(); +} + Future> fetchDevices() async { return Amplify.Auth.fetchDevices(); } 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 db135605d5..651cb92dcf 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 @@ -31,6 +31,7 @@ import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart ForgotPasswordRequest, GetUserAttributeVerificationCodeRequest, GetUserRequest, + GetDeviceRequest, ListDevicesRequest, ResendConfirmationCodeRequest, UserContextDataType, @@ -39,6 +40,7 @@ import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart VerifyUserAttributeRequest; import 'package:amplify_auth_cognito_dart/src/sdk/sdk_bridge.dart'; import 'package:amplify_auth_cognito_dart/src/sdk/src/cognito_identity_provider/model/analytics_metadata_type.dart'; +import 'package:amplify_auth_cognito_dart/src/sdk/src/cognito_identity_provider/model/get_device_response.dart'; import 'package:amplify_auth_cognito_dart/src/state/cognito_state_machine.dart'; import 'package:amplify_auth_cognito_dart/src/state/state.dart'; import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart'; @@ -97,6 +99,7 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface late CognitoAuthStateMachine _stateMachine = CognitoAuthStateMachine( dependencyManager: dependencies, ); + StreamSubscription? _stateMachineSubscription; /// The underlying state machine, for use in subclasses. @@ -993,6 +996,46 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface .result; } + @override + Future fetchCurrentDevice() async { + final tokens = await stateMachine.getUserPoolTokens(); + final deviceSecrets = await _deviceRepo.get(tokens.username); + final deviceKey = deviceSecrets?.deviceKey; + if (deviceSecrets == null || deviceKey == null) { + throw const DeviceNotTrackedException(); + } + + late GetDeviceResponse response; + + try { + response = await _cognitoIdp + .getDevice( + cognito.GetDeviceRequest( + deviceKey: deviceKey, + accessToken: tokens.accessToken.raw, + ), + ) + .result; + } on Exception catch (error) { + throw AuthException.fromException(error); + } + + final device = response.device; + final attributes = + device.deviceAttributes ?? const []; + + return CognitoDevice( + id: deviceKey, + attributes: { + for (final attribute in attributes) + attribute.name: attribute.value ?? '', + }, + createdDate: device.deviceCreateDate, + lastAuthenticatedDate: device.deviceLastAuthenticatedDate, + lastModifiedDate: device.deviceLastModifiedDate, + ); + } + @override Future> fetchDevices() async { final allDevices = []; diff --git a/packages/auth/amplify_auth_cognito_test/test/plugin/fetch_current_device_test.dart b/packages/auth/amplify_auth_cognito_test/test/plugin/fetch_current_device_test.dart new file mode 100644 index 0000000000..fa9405ee8c --- /dev/null +++ b/packages/auth/amplify_auth_cognito_test/test/plugin/fetch_current_device_test.dart @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +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/credentials/device_metadata_repository.dart'; +import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart'; +import 'package:amplify_auth_cognito_test/common/mock_clients.dart'; +import 'package:amplify_auth_cognito_test/common/mock_config.dart'; +import 'package:amplify_auth_cognito_test/common/mock_secure_storage.dart'; +import 'package:amplify_core/amplify_core.dart'; +import 'package:test/test.dart'; + +void main() { + AmplifyLogger().logLevel = LogLevel.verbose; + + final userPoolKeys = CognitoUserPoolKeys(userPoolConfig.appClientId); + final identityPoolKeys = CognitoIdentityPoolKeys(identityPoolConfig.poolId); + final testAuthRepo = AmplifyAuthProviderRepository(); + final mockDevice = DeviceType(deviceKey: deviceKey); + final mockDeviceResponse = GetDeviceResponse(device: mockDevice); + + late DeviceMetadataRepository repo; + late AmplifyAuthCognitoDart plugin; + + group('fetchCurrentDevice', () { + setUp(() async { + final secureStorage = MockSecureStorage(); + seedStorage( + secureStorage, + userPoolKeys: userPoolKeys, + identityPoolKeys: identityPoolKeys, + deviceKeys: CognitoDeviceKeys(userPoolConfig.appClientId, username), + ); + plugin = AmplifyAuthCognitoDart( + secureStorageFactory: (_) => secureStorage, + ); + await plugin.configure( + config: mockConfig, + authProviderRepo: testAuthRepo, + ); + repo = plugin.stateMachine.getOrCreate(); + }); + + group('should successfully', () { + setUp(() async { + final mockIdp = MockCognitoIdentityProviderClient( + getDevice: () async => mockDeviceResponse, + forgetDevice: () async {}, + ); + plugin.stateMachine.addInstance(mockIdp); + }); + + test( + 'return the current device where the current device id is equal to the local device id', + () async { + final secrets = await repo.get(username); + final currentDeviceKey = secrets?.deviceKey; + expect(currentDeviceKey, isNotNull); + final currentDevice = await plugin.fetchCurrentDevice(); + expect(currentDeviceKey, currentDevice.id); + }); + + test('throw a DeviceNotTrackedException when current device key is null', + () async { + await plugin.forgetDevice(); + await expectLater( + plugin.fetchCurrentDevice, + throwsA(isA()), + ); + }); + }); + + group('should throw', () { + setUp(() async { + final mockIdp = MockCognitoIdentityProviderClient( + getDevice: () async => throw AWSHttpException( + AWSHttpRequest.get(Uri.parse('https://aws.amazon.com/cognito/')), + ), + ); + plugin.stateMachine.addInstance(mockIdp); + }); + + test('a NetworkException', () async { + await expectLater( + plugin.fetchCurrentDevice, + throwsA(isA()), + ); + }); + }); + + tearDown(() async { + await plugin.close(); + }); + }); +} diff --git a/packages/test/amplify_integration_test/lib/src/stubs/amplify_auth_cognito_stub.dart b/packages/test/amplify_integration_test/lib/src/stubs/amplify_auth_cognito_stub.dart index d363d66ecc..c28eb95e6a 100644 --- a/packages/test/amplify_integration_test/lib/src/stubs/amplify_auth_cognito_stub.dart +++ b/packages/test/amplify_integration_test/lib/src/stubs/amplify_auth_cognito_stub.dart @@ -368,6 +368,13 @@ class AmplifyAuthCognitoStub extends AuthPluginInterface ); } + @override + Future fetchCurrentDevice() async { + throw UnimplementedError( + 'fetchCurrentDevice is not implemented.', + ); + } + @override Future forgetDevice([AuthDevice? device]) async { throw UnimplementedError( @@ -391,7 +398,6 @@ class AmplifyAuthCognitoStub extends AuthPluginInterface } class MockCognitoUser { - factory MockCognitoUser({ required String username, required String password,