Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(auth,api): cognito user pools auth provider & auth mode for API …
Browse files Browse the repository at this point in the history
…HTTP requests (#1913)
Travis Sheppard committed Sep 26, 2022
1 parent b5c9377 commit 4bc265e
Showing 6 changed files with 255 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -64,8 +64,15 @@ Future<http.BaseRequest> 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;
}
34 changes: 29 additions & 5 deletions packages/api/amplify_api/test/authorize_http_request_test.dart
Original file line number Diff line number Diff line change
@@ -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<ApiException>()));
});

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);

9 changes: 9 additions & 0 deletions packages/api/amplify_api/test/util.dart
Original file line number Diff line number Diff line change
@@ -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<AWSCredentials> retrieve() async {
@@ -43,6 +45,13 @@ class TestIamAuthProvider extends AWSIamAmplifyAuthProvider {
}
}

class TestTokenAuthProvider extends TokenAmplifyAuthProvider {
@override
Future<String> getLatestAuthToken() async {
return testAccessToken;
}
}

void validateSignedRequest(http.BaseRequest request) {
const userAgentHeader =
zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
Original file line number Diff line number Diff line change
@@ -53,6 +53,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';
@@ -261,10 +262,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) {
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<AuthSession> 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<CognitoIamAuthProvider>());
});
});

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<String>());
expect(credentials.secretAccessKey, isA<String>());
test('registers CognitoUserPoolsAuthProvider', () async {
final authProvider = testAuthRepo.getAuthProvider(
APIAuthorizationType.userPools.authProviderToken,
);
expect(authProvider, isA<CognitoUserPoolsAuthProvider>());
});
});

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<AmplifyException>()),
);
});

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<AuthException>()),
throwsA(isA<AmplifyException>()),
);
});
});

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<String>());
expect(credentials.secretAccessKey, isA<String>());
});

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<AuthException>()),
);
});
});

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<InvalidAccountTypeException>()),
);
});
});

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,
);
});
});
});
}

0 comments on commit 4bc265e

Please sign in to comment.