Skip to content

Commit

Permalink
feat(core,api): IAM auth mode for HTTP requests (REST and GQL) (#1893)
Browse files Browse the repository at this point in the history
  • Loading branch information
Travis Sheppard committed Oct 19, 2022
1 parent 4b88b43 commit 755970c
Show file tree
Hide file tree
Showing 12 changed files with 538 additions and 52 deletions.
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(
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

0 comments on commit 755970c

Please sign in to comment.