Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core,api): IAM auth mode for HTTP requests (REST and GQL) #1893

Merged
merged 78 commits into from
Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
03113a3
chore!(api): migrate API category type definitions
May 26, 2022
5f57bdc
deprecate rest exception
May 27, 2022
774d48e
change deprecation message
May 27, 2022
5198f9b
change constuctor
May 27, 2022
a63f126
Merge branch 'next' into chore/migrate-api-interface
May 31, 2022
3f1472a
change types for qp
May 31, 2022
c2aa31b
cleanup
May 31, 2022
f23b403
more cleanup
May 31, 2022
6fa8ded
fix response headers, other PT changes
Jun 1, 2022
5e993cb
Merge branch 'next' into chore/migrate-api-interface
Jun 1, 2022
1c9e8cb
change example app
Jun 2, 2022
780c654
Merge branch 'next' into chore/migrate-api-interface
Jun 2, 2022
6034d6b
feat(api): REST API in dart, auth mode NONE
Jun 2, 2022
5b8a8bd
Update packages/amplify_core/lib/src/types/api/rest/http_payload.dart
ragingsquirrel3 Jun 9, 2022
cfb1c4e
address some comments
Jun 9, 2022
828bde5
Merge branch 'next' into chore/migrate-api-interface
Jun 9, 2022
f756a17
address some comments
Jun 10, 2022
7dfad32
fix tests
Jun 13, 2022
fbaf480
Merge branch 'chore/migrate-api-interface' into feat/api-rest2
Jun 13, 2022
3f08e79
correct merge
Jun 13, 2022
ee4f068
feat(api): REST with IAM auth mode
Jun 13, 2022
a857b27
rough pass at auth providers
Jun 14, 2022
72f6e4b
address comments, json constructor
Jun 15, 2022
d983026
tweak json encoding
Jun 15, 2022
2ffa106
Merge branch 'next' into chore/migrate-api-interface
Jun 15, 2022
b66ef3a
Merge branch 'chore/migrate-api-interface' into feat/api-rest2
Jun 15, 2022
e5d4239
chore!(api): migrate API category type definitions (#1640)
Jun 15, 2022
909bb4f
Merge branch 'feat/api-next' into feat/api-rest2
Jun 21, 2022
c1fc4f5
add tests
Jun 21, 2022
66704de
correct qp types
Jun 21, 2022
daf21b3
Merge branch 'feat/api-rest2' into feat/api-iam
Jun 22, 2022
18d94ce
another pas of auth provider impl
Jun 22, 2022
8f9c6d8
chore(api): API Native Bridge for .addPlugin() (#1756)
Equartey Jun 23, 2022
9fb275f
revise auth provider impl
Jun 23, 2022
da4d327
Merge branch 'feat/api-next' into feat/api-rest2
Jun 24, 2022
05c9349
Merge branch 'feat/api-rest2' into feat/api-iam
Jun 24, 2022
e8d2e33
Merge branch 'feat/api-next' into feat/api-iam
Jul 5, 2022
1581006
correct some merge errors
Jul 5, 2022
93db7cb
correct some errors
Jul 5, 2022
bde7f2b
correct service type
Jul 5, 2022
9ff2ac1
reformat code
Jul 6, 2022
91f71cb
address some comments
Jul 11, 2022
ee71fb1
fix some tests and remove API changes
Jul 13, 2022
4f7e070
add tests for cognito auth providers
Jul 13, 2022
e3a49ae
Merge branch 'feat/api-next' into feat/api-iam
Jul 13, 2022
c385c7e
fix header
Jul 13, 2022
ee81e19
feat(api): REST and GQL query/mutate w IAM auth mode
Jul 13, 2022
de3697a
address some comments
Jul 14, 2022
4c0d048
Merge branch 'feat/api-next' into feat/api-iam
Jul 14, 2022
72bfa1a
fix some tests
Jul 14, 2022
f742aac
remove annotation
Jul 14, 2022
779b34a
correct test for chrome
Jul 14, 2022
7768971
Merge branch 'feat/api-iam' into feat/api-iam-auth-mode
Jul 15, 2022
f8fc89e
refactor types
Jul 15, 2022
3d21c0e
Merge branch 'feat/api-iam' into feat/api-iam-auth-mode
Jul 15, 2022
9dd7a84
change based on upstream
Jul 15, 2022
6b0ae89
change request type to AWS type
Jul 18, 2022
de52c8f
Merge branch 'feat/api-iam' into feat/api-iam-auth-mode
Jul 18, 2022
88967c4
change http client
Jul 18, 2022
7438727
refactor enum a little
Jul 18, 2022
cc1bf82
Merge branch 'feat/api-iam' into feat/api-iam-auth-mode
Jul 18, 2022
7d4d0e1
Update packages/auth/amplify_auth_cognito_dart/test/plugin/auth_provi…
ragingsquirrel3 Jul 18, 2022
4921b68
Update packages/auth/amplify_auth_cognito_dart/test/plugin/auth_provi…
ragingsquirrel3 Jul 18, 2022
cdae9a8
fix typo in comment
Jul 18, 2022
eb5f5ac
reformat a file
Jul 18, 2022
dc6e32d
Merge branch 'feat/api-iam' into feat/api-iam-auth-mode
Jul 18, 2022
ac6d53d
refactor w decorator
Jul 18, 2022
4aea273
Update packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_im…
ragingsquirrel3 Jul 19, 2022
f6395e3
address comment
Jul 19, 2022
37903c1
Merge branch 'feat/api-iam' of github.com:ragingsquirrel3/amplify-flu…
Jul 19, 2022
844c820
Merge branch 'feat/api-iam' into feat/api-iam-auth-mode
Jul 19, 2022
95e800a
Merge branch 'feat/api-next' into feat/api-iam-auth-mode
Jul 19, 2022
1a20381
add more tests
Jul 19, 2022
199c087
cleanup
Jul 19, 2022
cad4e17
sort
Jul 19, 2022
befa30e
tweak api key registration
Jul 19, 2022
4252d70
Update packages/amplify_core/lib/src/types/common/amplify_auth_provid…
ragingsquirrel3 Jul 21, 2022
288d44a
address some comments
Jul 21, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ enum APIAuthorizationType<T extends AmplifyAuthProvider> {
/// See also:
/// - [API Key Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#api-key-authorization)
@JsonValue('API_KEY')
apiKey(AmplifyAuthProviderToken<AmplifyAuthProvider>()),
apiKey(AmplifyAuthProviderToken<ApiKeyAmplifyAuthProvider>()),

/// Use an IAM access/secret key credential pair to authorize access to an API.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ class IamAuthProviderOptions extends AuthProviderOptions {
const IamAuthProviderOptions({required this.region, required this.service});
}

class ApiKeyAuthProviderOptions extends AuthProviderOptions {
final String apiKey;

const ApiKeyAuthProviderOptions(this.apiKey);
}

abstract class AmplifyAuthProvider {
Future<AWSBaseHttpRequest> authorizeRequest(
AWSBaseHttpRequest request, {
Expand All @@ -50,6 +56,14 @@ abstract class AWSIamAmplifyAuthProvider extends AmplifyAuthProvider
});
}

abstract class ApiKeyAmplifyAuthProvider extends AmplifyAuthProvider {
@override
Future<AWSBaseHttpRequest> authorizeRequest(
AWSBaseHttpRequest request, {
covariant ApiKeyAuthProviderOptions? options,
});
}

abstract class TokenAmplifyAuthProvider extends AmplifyAuthProvider {
Future<String> getLatestAuthToken();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,27 @@ import 'package:amplify_core/amplify_core.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

const _xApiKey = 'X-Api-Key';
import 'decorators/authorize_http_request.dart';

/// Implementation of http [http.Client] that authorizes HTTP requests with
/// Amplify.
@internal
class AmplifyAuthorizationRestClient extends http.BaseClient
implements Closeable {
/// [AmplifyAuthProviderRepository] for any auth modes this client may use.
final AmplifyAuthProviderRepository authProviderRepo;

/// Determines how requests with this client are authorized.
final AWSApiConfig endpointConfig;

final http.Client _baseClient;
final bool _useDefaultBaseClient;

/// Provide an [AWSApiConfig] which will determine how requests from this
/// client are authorized.
AmplifyAuthorizationRestClient({
required this.endpointConfig,
required this.authProviderRepo,
http.Client? baseClient,
}) : _useDefaultBaseClient = baseClient == null,
_baseClient = baseClient ?? http.Client();
Expand All @@ -42,27 +47,14 @@ class AmplifyAuthorizationRestClient extends http.BaseClient
/// header already set.
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async =>
_baseClient.send(_authorizeRequest(request));
_baseClient.send(await authorizeHttpRequest(
request,
endpointConfig: endpointConfig,
authProviderRepo: authProviderRepo,
));

@override
void close() {
if (_useDefaultBaseClient) _baseClient.close();
}

http.BaseRequest _authorizeRequest(http.BaseRequest request) {
if (!request.headers.containsKey(AWSHeaders.authorization) &&
endpointConfig.authorizationType != APIAuthorizationType.none) {
// TODO(ragingsquirrel3): Use auth providers from core to transform the request.
final apiKey = endpointConfig.apiKey;
if (endpointConfig.authorizationType == APIAuthorizationType.apiKey) {
if (apiKey == null) {
throw const ApiException(
'Auth mode is API Key, but no API Key was found in config.');
}

request.headers.putIfAbsent(_xApiKey, () => apiKey);
}
}
return request;
}
}
64 changes: 36 additions & 28 deletions packages/api/amplify_api/lib/src/api_plugin_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'package:meta/meta.dart';

import 'amplify_api_config.dart';
import 'amplify_authorization_rest_client.dart';
import 'graphql/app_sync_api_key_auth_provider.dart';
import 'graphql/send_graphql_request.dart';
import 'util.dart';

Expand All @@ -35,10 +36,11 @@ import 'util.dart';
class AmplifyAPIDart extends AmplifyAPI {
late final AWSApiPluginConfig _apiConfig;
final http.Client? _baseHttpClient;
late final AmplifyAuthProviderRepository _authProviderRepo;

/// A map of the keys from the Amplify API config to HTTP clients to use for
/// requests to that endpoint.
final Map<String, AmplifyAuthorizationRestClient> _clientPool = {};
final Map<String, http.Client> _clientPool = {};

/// The registered [APIAuthProvider] instances.
final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};
Expand All @@ -65,6 +67,21 @@ class AmplifyAPIDart extends AmplifyAPI {
'https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/flutter/#configure-api');
}
_apiConfig = apiConfig;
_authProviderRepo = authProviderRepo;
_registerApiPluginAuthProviders();
}

/// If an endpoint has an API key, ensure valid auth provider registered.
void _registerApiPluginAuthProviders() {
_apiConfig.endpoints.forEach((key, value) {
// Check the presence of apiKey (not auth type) because other modes might
// have a key if not the primary auth mode.
if (value.apiKey != null) {
_authProviderRepo.registerAuthProvider(
value.authorizationType.authProviderToken,
AppSyncApiKeyAuthProvider());
}
});
}

@override
Expand All @@ -89,32 +106,21 @@ class AmplifyAPIDart extends AmplifyAPI {
}
}

/// Returns the HTTP client to be used for GraphQL operations.
/// Returns the HTTP client to be used for REST/GraphQL operations.
///
/// Use [apiName] if there are multiple GraphQL endpoints.
/// Use [apiName] if there are multiple endpoints of the same type.
@visibleForTesting
http.Client getGraphQLClient({String? apiName}) {
http.Client getHttpClient(EndpointType type, {String? apiName}) {
final endpoint = _apiConfig.getEndpoint(
type: EndpointType.graphQL,
type: type,
apiName: apiName,
);
return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
endpointConfig: endpoint.config, baseClient: _baseHttpClient);
}

/// Returns the HTTP client to be used for REST operations.
///
/// Use [apiName] if there are multiple REST endpoints.
@visibleForTesting
http.Client getRestClient({String? apiName}) {
final endpoint = _apiConfig.getEndpoint(
type: EndpointType.rest,
apiName: apiName,
);
return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
return _clientPool[endpoint.name] ??= AmplifyHttpClient(
ragingsquirrel3 marked this conversation as resolved.
Show resolved Hide resolved
baseClient: AmplifyAuthorizationRestClient(
endpointConfig: endpoint.config,
baseClient: _baseHttpClient,
);
authProviderRepo: _authProviderRepo,
));
}

Uri _getGraphQLUri(String? apiName) {
Expand Down Expand Up @@ -160,7 +166,8 @@ class AmplifyAPIDart extends AmplifyAPI {
@override
CancelableOperation<GraphQLResponse<T>> query<T>(
{required GraphQLRequest<T> request}) {
final graphQLClient = getGraphQLClient(apiName: request.apiName);
final graphQLClient =
getHttpClient(EndpointType.graphQL, apiName: request.apiName);
final uri = _getGraphQLUri(request.apiName);

final responseFuture = sendGraphQLRequest<T>(
Expand All @@ -171,7 +178,8 @@ class AmplifyAPIDart extends AmplifyAPI {
@override
CancelableOperation<GraphQLResponse<T>> mutate<T>(
{required GraphQLRequest<T> request}) {
final graphQLClient = getGraphQLClient(apiName: request.apiName);
final graphQLClient =
getHttpClient(EndpointType.graphQL, apiName: request.apiName);
final uri = _getGraphQLUri(request.apiName);

final responseFuture = sendGraphQLRequest<T>(
Expand All @@ -190,7 +198,7 @@ class AmplifyAPIDart extends AmplifyAPI {
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
final client = getHttpClient(EndpointType.rest, apiName: apiName);
return _prepareRestResponse(AWSStreamedHttpRequest.delete(
uri,
body: body ?? HttpPayload.empty(),
Expand All @@ -206,7 +214,7 @@ class AmplifyAPIDart extends AmplifyAPI {
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
final client = getHttpClient(EndpointType.rest, apiName: apiName);
return _prepareRestResponse(
AWSHttpRequest.get(
uri,
Expand All @@ -223,7 +231,7 @@ class AmplifyAPIDart extends AmplifyAPI {
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
final client = getHttpClient(EndpointType.rest, apiName: apiName);
return _prepareRestResponse(
AWSHttpRequest.head(
uri,
Expand All @@ -241,7 +249,7 @@ class AmplifyAPIDart extends AmplifyAPI {
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
final client = getHttpClient(EndpointType.rest, apiName: apiName);
return _prepareRestResponse(
AWSStreamedHttpRequest.patch(
uri,
Expand All @@ -260,7 +268,7 @@ class AmplifyAPIDart extends AmplifyAPI {
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
final client = getHttpClient(EndpointType.rest, apiName: apiName);
return _prepareRestResponse(
AWSStreamedHttpRequest.post(
uri,
Expand All @@ -279,7 +287,7 @@ class AmplifyAPIDart extends AmplifyAPI {
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
final client = getHttpClient(EndpointType.rest, apiName: apiName);
return _prepareRestResponse(
AWSStreamedHttpRequest.put(
uri,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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_core/amplify_core.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

/// Transforms an HTTP request according to auth providers that match the endpoint
/// configuration.
@internal
Future<http.BaseRequest> authorizeHttpRequest(http.BaseRequest request,
{required AWSApiConfig endpointConfig,
required AmplifyAuthProviderRepository authProviderRepo}) async {
if (request.headers.containsKey(AWSHeaders.authorization)) {
return request;
}
final authType = endpointConfig.authorizationType;

switch (authType) {
case APIAuthorizationType.apiKey:
final authProvider = _validateAuthProvider(
authProviderRepo
.getAuthProvider(APIAuthorizationType.apiKey.authProviderToken),
authType);
final apiKey = endpointConfig.apiKey;
if (apiKey == null) {
throw const ApiException(
'Auth mode is API Key, but no API Key was found in config.');
}

final authorizedRequest = await authProvider.authorizeRequest(
_httpToAWSRequest(request),
options: ApiKeyAuthProviderOptions(apiKey));
return authorizedRequest.httpRequest;
case APIAuthorizationType.iam:
final authProvider = _validateAuthProvider(
authProviderRepo
.getAuthProvider(APIAuthorizationType.iam.authProviderToken),
authType);
final service = endpointConfig.endpointType == EndpointType.graphQL
? AWSService.appSync
: AWSService.apiGatewayManagementApi; // resolves to "execute-api"

final authorizedRequest = await authProvider.authorizeRequest(
_httpToAWSRequest(request),
options: IamAuthProviderOptions(
region: endpointConfig.region,
service: service,
),
);
return authorizedRequest.httpRequest;
case APIAuthorizationType.function:
case APIAuthorizationType.oidc:
case APIAuthorizationType.userPools:
throw UnimplementedError('${authType.name} not implemented.');
case APIAuthorizationType.none:
return request;
}
}

T _validateAuthProvider<T extends AmplifyAuthProvider>(
T? authProvider, APIAuthorizationType authType) {
if (authProvider == null) {
throw ApiException('No auth provider found for auth mode ${authType.name}.',
recoverySuggestion: 'Ensure auth plugin correctly configured.');
}
return authProvider;
}

AWSBaseHttpRequest _httpToAWSRequest(http.BaseRequest request) {
final method = AWSHttpMethod.fromString(request.method);
if (request is http.Request) {
return AWSHttpRequest(
method: method,
uri: request.url,
headers: {
AWSHeaders.contentType: 'application/x-amz-json-1.1',
...request.headers,
},
body: request.bodyBytes,
);
} else if (request is http.StreamedRequest) {
return AWSStreamedHttpRequest(
method: method,
uri: request.url,
headers: {
AWSHeaders.contentType: 'application/x-amz-json-1.1',
...request.headers,
},
body: request.finalize(),
);
} else {
throw UnimplementedError(
'Multipart HTTP requests are not supported.',
);
}
}
Loading