Skip to content

Commit

Permalink
feat(Auth): Add fetchCurrentDevice API (#5251)
Browse files Browse the repository at this point in the history
feat(Auth): Add fetchCurrentDevice API (#5251)
  • Loading branch information
hahnandrew authored Aug 14, 2024
1 parent ee26d74 commit c6a06ef
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 2 deletions.
11 changes: 11 additions & 0 deletions packages/amplify_core/doc/lib/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,17 @@ Future<void> forgetSpecificDevice(AuthDevice myDevice) async {
}
// #enddocregion forget-specific-device

// #docregion fetch-current-device
Future<void> 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<void> fetchAllDevices() async {
try {
Expand Down
31 changes: 31 additions & 0 deletions packages/amplify_core/lib/src/category/amplify_auth_category.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,37 @@ class AuthCategory extends AmplifyCategory<AuthPluginInterface> {
() => 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
///
/// <?code-excerpt "doc/lib/auth.dart" region="imports"?>
/// ```dart
/// import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
/// import 'package:amplify_flutter/amplify_flutter.dart';
/// ```
///
/// <?code-excerpt "doc/lib/auth.dart" region="fetch-current-device"?>
/// ```dart
/// Future<AuthDevice> 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<AuthDevice> fetchCurrentDevice() => identifyCall(
AuthCategoryMethod.fetchCurrentDevice,
() => defaultPlugin.fetchCurrentDevice(),
);

/// {@template amplify_core.amplify_auth_category.forget_device}
/// Forgets the current device.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ enum AuthCategoryMethod with AmplifyCategoryMethod {
setMfaPreference('49'),
getMfaPreference('50'),
setUpTotp('51'),
verifyTotpSetup('52');
verifyTotpSetup('52'),
fetchCurrentDevice('59');

const AuthCategoryMethod(this.method);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthDevice> fetchCurrentDevice() {
throw UnimplementedError('fetchCurrentDevice() has not been implemented.');
}

/// {@macro amplify_core.amplify_auth_category.fetch_devices}
Future<List<AuthDevice>> fetchDevices() {
throw UnimplementedError('fetchDevices() has not been implemented.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeviceNotTrackedException>()),
);
});

asyncTest(
'fetchCurrentDevice throws a SignedOutException when device signs out.',
(_) async {
await signOutUser();
await expectLater(
Amplify.Auth.fetchCurrentDevice,
throwsA(isA<SignedOutException>()),
);
});

asyncTest('forgetDevice stops tracking', (_) async {
expect(await getDeviceState(), DeviceState.remembered);
await Amplify.Auth.forgetDevice();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ Future<List<AuthUserAttribute>> fetchUserAttributes() async {
return Amplify.Auth.fetchUserAttributes();
}

Future<AuthDevice> fetchCurrentDevice() async {
return Amplify.Auth.fetchCurrentDevice();
}

Future<List<AuthDevice>> fetchDevices() async {
return Amplify.Auth.fetchDevices();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart
ForgotPasswordRequest,
GetUserAttributeVerificationCodeRequest,
GetUserRequest,
GetDeviceRequest,
ListDevicesRequest,
ResendConfirmationCodeRequest,
UserContextDataType,
Expand All @@ -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';
Expand Down Expand Up @@ -97,6 +99,7 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
late CognitoAuthStateMachine _stateMachine = CognitoAuthStateMachine(
dependencyManager: dependencies,
);

StreamSubscription<AuthState>? _stateMachineSubscription;

/// The underlying state machine, for use in subclasses.
Expand Down Expand Up @@ -993,6 +996,46 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
.result;
}

@override
Future<CognitoDevice> 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 <cognito.AttributeType>[];

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<List<CognitoDevice>> fetchDevices() async {
final allDevices = <CognitoDevice>[];
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DeviceMetadataRepository>();
});

group('should successfully', () {
setUp(() async {
final mockIdp = MockCognitoIdentityProviderClient(
getDevice: () async => mockDeviceResponse,
forgetDevice: () async {},
);
plugin.stateMachine.addInstance<CognitoIdentityProviderClient>(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<DeviceNotTrackedException>()),
);
});
});

group('should throw', () {
setUp(() async {
final mockIdp = MockCognitoIdentityProviderClient(
getDevice: () async => throw AWSHttpException(
AWSHttpRequest.get(Uri.parse('https://aws.amazon.com/cognito/')),
),
);
plugin.stateMachine.addInstance<CognitoIdentityProviderClient>(mockIdp);
});

test('a NetworkException', () async {
await expectLater(
plugin.fetchCurrentDevice,
throwsA(isA<NetworkException>()),
);
});
});

tearDown(() async {
await plugin.close();
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,13 @@ class AmplifyAuthCognitoStub extends AuthPluginInterface
);
}

@override
Future<AuthDevice> fetchCurrentDevice() async {
throw UnimplementedError(
'fetchCurrentDevice is not implemented.',
);
}

@override
Future<void> forgetDevice([AuthDevice? device]) async {
throw UnimplementedError(
Expand All @@ -391,7 +398,6 @@ class AmplifyAuthCognitoStub extends AuthPluginInterface
}

class MockCognitoUser {

factory MockCognitoUser({
required String username,
required String password,
Expand Down

0 comments on commit c6a06ef

Please sign in to comment.