From 64a50f620cfcd391aeec659c45105767453db9fc Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 8 Aug 2022 08:58:01 -0800 Subject: [PATCH] feat(auth,api): cognito user pools auth provider & auth mode for API HTTP requests (#1913) --- .../decorators/authorize_http_request.dart | 9 +- .../test/authorize_http_request_test.dart | 34 ++- packages/api/amplify_api/test/util.dart | 9 + .../lib/src/auth_plugin_impl.dart | 14 +- .../cognito_user_pools_auth_provider.dart | 37 +++ .../test/plugin/auth_providers_test.dart | 210 ++++++++++++++---- 6 files changed, 255 insertions(+), 58 deletions(-) create mode 100644 packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart diff --git a/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart index 3cab4d7443..24a343895e 100644 --- a/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart +++ b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart @@ -64,8 +64,15 @@ Future authorizeHttpRequest(http.BaseRequest request, return authorizedRequest.httpRequest; case APIAuthorizationType.function: case APIAuthorizationType.oidc: - case APIAuthorizationType.userPools: throw UnimplementedError('${authType.name} not implemented.'); + case APIAuthorizationType.userPools: + final authProvider = _validateAuthProvider( + authProviderRepo.getAuthProvider(authType.authProviderToken), + authType, + ); + final authorizedRequest = + await authProvider.authorizeRequest(_httpToAWSRequest(request)); + return authorizedRequest.httpRequest; case APIAuthorizationType.none: return request; } diff --git a/packages/api/amplify_api/test/authorize_http_request_test.dart b/packages/api/amplify_api/test/authorize_http_request_test.dart index 2179a07ad8..3f1ad3754d 100644 --- a/packages/api/amplify_api/test/authorize_http_request_test.dart +++ b/packages/api/amplify_api/test/authorize_http_request_test.dart @@ -33,11 +33,19 @@ void main() { final authProviderRepo = AmplifyAuthProviderRepository(); setUpAll(() { - authProviderRepo.registerAuthProvider( + authProviderRepo + ..registerAuthProvider( APIAuthorizationType.apiKey.authProviderToken, - AppSyncApiKeyAuthProvider()); - authProviderRepo.registerAuthProvider( - APIAuthorizationType.iam.authProviderToken, TestIamAuthProvider()); + AppSyncApiKeyAuthProvider(), + ) + ..registerAuthProvider( + APIAuthorizationType.iam.authProviderToken, + TestIamAuthProvider(), + ) + ..registerAuthProvider( + APIAuthorizationType.userPools.authProviderToken, + TestTokenAuthProvider(), + ); }); group('authorizeHttpRequest', () { @@ -132,7 +140,23 @@ void main() { throwsA(isA())); }); - test('authorizes with Cognito User Pools auth mode', () {}, skip: true); + test('authorizes with Cognito User Pools auth mode', () async { + const endpointConfig = AWSApiConfig( + authorizationType: APIAuthorizationType.userPools, + endpoint: _gqlEndpoint, + endpointType: EndpointType.graphQL, + region: _region); + final inputRequest = _generateTestRequest(endpointConfig.endpoint); + final authorizedRequest = await authorizeHttpRequest( + inputRequest, + endpointConfig: endpointConfig, + authProviderRepo: authProviderRepo, + ); + expect( + authorizedRequest.headers[AWSHeaders.authorization], + testAccessToken, + ); + }); test('authorizes with OIDC auth mode', () {}, skip: true); diff --git a/packages/api/amplify_api/test/util.dart b/packages/api/amplify_api/test/util.dart index f3c2ef551e..cd06f8c13c 100644 --- a/packages/api/amplify_api/test/util.dart +++ b/packages/api/amplify_api/test/util.dart @@ -17,6 +17,8 @@ import 'package:aws_signature_v4/aws_signature_v4.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; +const testAccessToken = 'test-access-token-123'; + class TestIamAuthProvider extends AWSIamAmplifyAuthProvider { @override Future retrieve() async { @@ -43,6 +45,13 @@ class TestIamAuthProvider extends AWSIamAmplifyAuthProvider { } } +class TestTokenAuthProvider extends TokenAmplifyAuthProvider { + @override + Future getLatestAuthToken() async { + return testAccessToken; + } +} + void validateSignedRequest(http.BaseRequest request) { const userAgentHeader = zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent; 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 1db9c30481..d1b1810f6e 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 @@ -52,6 +52,7 @@ import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart import 'package:amplify_auth_cognito_dart/src/sdk/sdk_bridge.dart'; import 'package:amplify_auth_cognito_dart/src/state/state.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'; import 'package:amplify_secure_storage_dart/amplify_secure_storage_dart.dart'; import 'package:built_collection/built_collection.dart'; @@ -185,10 +186,15 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface // Register auth providers to provide auth functionality to other plugins // without requiring other plugins to call `Amplify.Auth...` directly. - authProviderRepo.registerAuthProvider( - APIAuthorizationType.iam.authProviderToken, - CognitoIamAuthProvider(), - ); + authProviderRepo + ..registerAuthProvider( + APIAuthorizationType.iam.authProviderToken, + CognitoIamAuthProvider(), + ) + ..registerAuthProvider( + APIAuthorizationType.userPools.authProviderToken, + CognitoUserPoolsAuthProvider(), + ); if (_stateMachine.getOrCreate(AuthStateMachine.type).currentState.type != AuthStateType.notConfigured) { 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 new file mode 100644 index 0000000000..edde7c3bca --- /dev/null +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart @@ -0,0 +1,37 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart'; +import 'package:amplify_core/amplify_core.dart'; +import 'package:meta/meta.dart'; + +/// [AmplifyAuthProvider] implementation that adds access token to request headers. +@internal +class CognitoUserPoolsAuthProvider extends TokenAmplifyAuthProvider { + /// Get access token from `Amplify.Auth.fetchAuthSession()`. + @override + Future getLatestAuthToken() async { + final authSession = + await Amplify.Auth.fetchAuthSession() as CognitoAuthSession; + final token = authSession.userPoolTokens?.accessToken.raw; + if (token == null) { + throw const AuthException( + 'Unable to fetch access token while authorizing with Cognito User Pools.', + ); + } + return token; + } +} diff --git a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart index acb126fa66..de1d20b496 100644 --- a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart +++ b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart @@ -15,7 +15,9 @@ import 'dart:async'; 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/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'; import 'package:test/test.dart'; @@ -29,84 +31,196 @@ AWSHttpRequest _generateTestRequest() { ); } -/// Returns dummy AWS credentials. -class TestAmplifyAuth extends AmplifyAuthCognitoDart { +/// Mock implementation of user pool only error when trying to get credentials. +class TestAmplifyAuthUserPoolOnly extends AmplifyAuthCognitoDart { @override Future fetchAuthSession({ required AuthSessionRequest request, }) async { - return const CognitoAuthSession( + final options = request.options as CognitoSessionOptions?; + 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, - credentials: AWSCredentials('fakeKeyId', 'fakeSecret'), + userPoolTokens: CognitoUserPoolTokens( + accessToken: accessToken, + idToken: idToken, + refreshToken: refreshToken, + ), ); } } void main() { + late AmplifyAuthCognitoDart plugin; + late AmplifyAuthProviderRepository testAuthRepo; + + setUpAll(() async { + testAuthRepo = AmplifyAuthProviderRepository(); + final secureStorage = MockSecureStorage(); + final stateMachine = CognitoAuthStateMachine()..addInstance(secureStorage); + plugin = AmplifyAuthCognitoDart(credentialStorage: secureStorage) + ..stateMachine = stateMachine; + + seedStorage( + secureStorage, + userPoolKeys: CognitoUserPoolKeys(userPoolConfig), + identityPoolKeys: CognitoIdentityPoolKeys(identityPoolConfig), + ); + + await plugin.configure( + config: mockConfig, + authProviderRepo: testAuthRepo, + ); + }); + group( 'AmplifyAuthCognitoDart plugin registers auth providers during configuration', () { - late AmplifyAuthCognitoDart plugin; - - setUp(() async { - plugin = AmplifyAuthCognitoDart(credentialStorage: MockSecureStorage()); - }); - test('registers CognitoIamAuthProvider', () async { - final testAuthRepo = AmplifyAuthProviderRepository(); - await plugin.configure( - config: mockConfig, - authProviderRepo: testAuthRepo, - ); final authProvider = testAuthRepo.getAuthProvider( APIAuthorizationType.iam.authProviderToken, ); expect(authProvider, isA()); }); - }); - - group('CognitoIamAuthProvider', () { - setUpAll(() async { - await Amplify.addPlugin(TestAmplifyAuth()); - }); - test('gets AWS credentials from Amplify.Auth.fetchAuthSession', () async { - final authProvider = CognitoIamAuthProvider(); - final credentials = await authProvider.retrieve(); - expect(credentials.accessKeyId, isA()); - expect(credentials.secretAccessKey, isA()); + test('registers CognitoUserPoolsAuthProvider', () async { + final authProvider = testAuthRepo.getAuthProvider( + APIAuthorizationType.userPools.authProviderToken, + ); + expect(authProvider, isA()); }); + }); - test('signs a request when calling authorizeRequest', () async { + group('no auth plugin added', () { + test('CognitoIamAuthProvider throws when trying to authorize a request', + () async { final authProvider = CognitoIamAuthProvider(); - final authorizedRequest = await authProvider.authorizeRequest( - _generateTestRequest(), - options: const IamAuthProviderOptions( - region: 'us-east-1', - service: AWSService.appSync, + await expectLater( + authProvider.authorizeRequest( + _generateTestRequest(), + options: const IamAuthProviderOptions( + region: 'us-east-1', + service: AWSService.appSync, + ), ), - ); - // Note: not intended to be complete test of sigv4 algorithm. - expect(authorizedRequest.headers[AWSHeaders.authorization], isNotEmpty); - const userAgentHeader = - zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent; - expect( - authorizedRequest.headers[AWSHeaders.host], - isNotEmpty, - skip: zIsWeb, - ); - expect( - authorizedRequest.headers[userAgentHeader], - contains('aws-sigv4'), + throwsA(isA()), ); }); - test('throws when no options provided', () async { - final authProvider = CognitoIamAuthProvider(); + test('CognitoUserPoolsAuthProvider throws when trying to authorize request', + () async { + final authProvider = CognitoUserPoolsAuthProvider(); await expectLater( authProvider.authorizeRequest(_generateTestRequest()), - throwsA(isA()), + throwsA(isA()), ); }); }); + + group('auth providers defined in auth plugin', () { + setUpAll(() async { + await Amplify.reset(); + await Amplify.addPlugin(plugin); + }); + + group('CognitoIamAuthProvider', () { + test('gets AWS credentials from Amplify.Auth.fetchAuthSession', () async { + final authProvider = CognitoIamAuthProvider(); + final credentials = await authProvider.retrieve(); + expect(credentials.accessKeyId, isA()); + expect(credentials.secretAccessKey, isA()); + }); + + test('signs a request when calling authorizeRequest', () async { + final authProvider = CognitoIamAuthProvider(); + final authorizedRequest = await authProvider.authorizeRequest( + _generateTestRequest(), + options: const IamAuthProviderOptions( + region: 'us-east-1', + service: AWSService.appSync, + ), + ); + // Note: not intended to be complete test of sigv4 algorithm. + expect(authorizedRequest.headers[AWSHeaders.authorization], isNotEmpty); + const userAgentHeader = + zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent; + expect( + authorizedRequest.headers[AWSHeaders.host], + isNotEmpty, + skip: zIsWeb, + ); + expect( + authorizedRequest.headers[userAgentHeader], + contains('aws-sigv4'), + ); + }); + + test('throws when no options provided', () async { + final authProvider = CognitoIamAuthProvider(); + await expectLater( + authProvider.authorizeRequest(_generateTestRequest()), + throwsA(isA()), + ); + }); + }); + + group('CognitoUserPoolsAuthProvider', () { + test('gets raw access token from Amplify.Auth.fetchAuthSession', + () async { + final authProvider = CognitoUserPoolsAuthProvider(); + final token = await authProvider.getLatestAuthToken(); + expect(token, accessToken.raw); + }); + + test('adds access token to header when calling authorizeRequest', + () async { + final authProvider = CognitoUserPoolsAuthProvider(); + final authorizedRequest = await authProvider.authorizeRequest( + _generateTestRequest(), + ); + expect( + authorizedRequest.headers[AWSHeaders.authorization], + accessToken.raw, + ); + }); + }); + }); + + group('auth providers with user pool-only configuration', () { + setUpAll(() async { + await Amplify.reset(); + await Amplify.addPlugin(TestAmplifyAuthUserPoolOnly()); + }); + + group('CognitoIamAuthProvider', () { + test('throws when trying to retrieve credentials', () async { + final authProvider = CognitoIamAuthProvider(); + await expectLater( + authProvider.retrieve(), + throwsA(isA()), + ); + }); + }); + + group('CognitoUserPoolsAuthProvider', () { + test('adds access token to header when calling authorizeRequest', + () async { + final authProvider = CognitoUserPoolsAuthProvider(); + final authorizedRequest = await authProvider.authorizeRequest( + _generateTestRequest(), + ); + expect( + authorizedRequest.headers[AWSHeaders.authorization], + accessToken.raw, + ); + }); + }); + }); }