API GatewayのLambdaオーソライザー機能を簡単にデプロイしてみた
今回やること
API Gateway の REST API で、簡単な Lambda オーソライザーを加えた構成を CDK(Python) で一括で実装してみたいと思います.
Lambda オーソライザーとは?
API Gateway で実装した API にリクエストする際、前処理としての Lambda 処理を実装することができ、そこでリクエストの認可処理を加えることができます。
CDK で一括構築
具体的なディレクトリ構成、CDK の前準備などは今回は割愛します.
api_gateway_stack.py
from aws_cdk import ( Duration, Stack, AssetHashType, BundlingOptions, aws_logs as logs, aws_lambda as lambda_, aws_apigateway as apigateway, aws_iam as iam ) from constructs import Construct class ApiGatewayStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) authorizer_function = lambda_.Function( self, "token_authorizer_handler", runtime=lambda_.Runtime.PYTHON_3_9, code=lambda_.Code.from_asset( '../lambda/src/authorizer', asset_hash_type=AssetHashType.SOURCE, bundling=BundlingOptions( image=lambda_.Runtime.PYTHON_3_9.bundling_image, command=[ "bash", "-c", " && ".join( [ "pip install -r requirements.txt -t /asset-output", "cp -au . /asset-output", ] ), ], user="root:root" ), ), handler="app.handler", environment={}, timeout=Duration.seconds( 29), memory_size=512, log_retention=logs.RetentionDays.TWO_MONTHS ) lambda_.Alias(self, 'token_authorizer_alias_live', alias_name='Live', version=authorizer_function.current_version ) api_test_func = lambda_.Function( self, "test_api_handler", runtime=lambda_.Runtime.PYTHON_3_9, code=lambda_.Code.from_asset( '../lambda/src/test_api', asset_hash_type=AssetHashType.SOURCE, bundling=BundlingOptions( image=lambda_.Runtime.PYTHON_3_9.bundling_image, command=[ "bash", "-c", " && ".join( [ "pip install -r requirements.txt -t /asset-output", "cp -au . /asset-output", ] ), ], user="root:root" ), ), handler="app.handler", environment={}, timeout=Duration.seconds( 29), memory_size=512, log_retention=logs.RetentionDays.TWO_MONTHS ) lambda_.Alias(self, 'test_apiLambda_alias_live', alias_name='Live', version=api_test_func.current_version ) apigw_auth_role = iam.Role( self, 'apigateway_auth_role', assumed_by=iam.ServicePrincipal( service='apigateway.amazonaws.com' ) ) apigw_auth_statement = iam.PolicyStatement( actions=[ 'lambda:InvokeFunction' ], resources=[ '*' ] ) apigw_auth_role.add_to_policy(apigw_auth_statement) apigw_auth = apigateway.TokenAuthorizer( self, 'apigateway_tokenauthorizer', handler=authorizer_function, assume_role=apigw_auth_role, validation_regex='^Bearer [-0-9a-zA-z\.]*$', results_cache_ttl=Duration.minutes(0) ) api = apigateway.RestApi(self, "sample_apigateway") user_univ_token_api = api.root.add_resource( "user").add_resource("{universal_id}").add_resource("token") user_univ_token_api.add_method( "GET", apigateway.LambdaIntegration(api_test_func), authorization_type=apigateway.AuthorizationType.CUSTOM, authorizer=apigw_auth )
Lambda オーソラーザーにはトークンベースとリクエストベースがありますが今回はトークンベースを設定しました.
apigateway.TokenAuthorizer()を使用し、
validation_regex の項目で Lambda に到達する前にバリデーションをかけることができます.
また、results_cache_ttl の項目でキャッシュ期間を設定できますが、今回は検証のためにキャッシュしないようにしています.
(キャッシュ期間を設定すると、設定期間の間 Lambda が動くことなくレスポンスをしてくれるようになりますが、返却するポリシードキュメントのスコープもキャッシュされることになるので複数の API を後続に設定する場合は注意が必要なようです。機会があれば今後この辺りの検証も記事にしたいと思います。)
authorizer/app
def handler(event, context): try: print(event) authorizationToken = event.get('authorizationToken') policy_statement = { 'Action': 'execute-api:Invoke', 'Resource': '*' } if authorizationToken == 'Bearer 123': policy_statement['Effect'] = 'Allow' else: policy_statement['Effect'] = 'Deny' response = { 'principalId': 'abc123', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [policy_statement] }, 'context': { 'id': 'test1234', 'meta': 'meta' } } print(response) return response except Exception as e: print(e) raise Exception('Unauthorized')
Lambda オーソライザーには以下のような event が入力されてきます
{ 'authorizationToken': 'Bearer 123', 'methodArn': 'arn:aws:execute-api:ap-northeast-1:00000000000:xxxxxxxxxx/dev/GET/user/12345/token', 'type': 'TOKEN' }
返り値では以下のように、policyDocument.Statement の Effect を Allow で返すか Deny で返すかでアクセスの許可拒否を制御できます
また、context で後続の lambda に渡したいデータを入力します
{ 'principalId': 'abc123', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [ { 'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': '*' } ] }, 'context': { 'id': 'test1234', 'meta': 'context' } }
test_api/app
import json import ulid def handler(event, context): try: print(event) universal_id = event["pathParameters"]["universal_id"] ulid_value = ulid.new().str if universal_id == 'xx': print("403!!", universal_id) raise Exception("Error occured!!") print("200!!", universal_id) body = { 'id': ulid_value, 'token': f'token-{universal_id}' } dumpbody = json.dumps(body) print("dumpbody", dumpbody) return { 'statusCode': 200, 'body': dumpbody } except Exception as e: print(e) return { 'statusCode': 403, 'body': f'message:{e}' }
ここでの event は以下です オーソライザーで返した Context の内容は、requestContext.authorizer 以下に出力されていました
{ 'body': 'None', 'headers': {'Accept': '*/*', 'Authorization': 'Bearer 123', 'CloudFront-Forwarded-Proto': 'https', 'CloudFront-Is-Desktop-Viewer': 'true', 'CloudFront-Is-Mobile-Viewer': 'false', 'CloudFront-Is-SmartTV-Viewer': 'false', 'CloudFront-Is-Tablet-Viewer': 'false', 'CloudFront-Viewer-ASN': '16509', 'CloudFront-Viewer-Country': 'JP', 'Host': 'xxxxxxx.execute-api.ap-northeast-1.amazonaws.com', 'User-Agent': 'curl/7.79.1', 'Via': '2.0 xxxxxxxxxx.cloudfront.net ' '(CloudFront)', 'X-Amz-Cf-Id': 'xxxxxxxxxxxxxxxxxxx==', 'X-Amzn-Trace-Id': 'Root=1-62eb680a-xxxxxxxxxxxxxxxx', 'X-Forwarded-For': 'xx.xx.xx.xx, 64.xxx.xxx.142', 'X-Forwarded-Port': '443', 'X-Forwarded-Proto': 'https'}, 'httpMethod': 'GET', 'isBase64Encoded': False, 'multiValueHeaders': {'Accept': ['*/*'], 'Authorization': ['Bearer 123'], 'CloudFront-Forwarded-Proto': ['https'], 'CloudFront-Is-Desktop-Viewer': ['true'], 'CloudFront-Is-Mobile-Viewer': ['false'], 'CloudFront-Is-SmartTV-Viewer': ['false'], 'CloudFront-Is-Tablet-Viewer': ['false'], 'CloudFront-Viewer-ASN': ['16509'], 'CloudFront-Viewer-Country': ['JP'], 'Host': ['w7yk1fxfi3.execute-api.ap-northeast-1.amazonaws.com'], 'User-Agent': ['curl/7.79.1'], 'Via': ['2.0 ' 'xxxxxxxxx.cloudfront.net ' '(CloudFront)'], 'X-Amz-Cf-Id': ['GplUu7QrBkWWwu_RKSuS9iRthezNLcBU6BcvIg40pz53CuyUJUiZRg=='], 'X-Amzn-Trace-Id': ['Root=1-62eb680a-3ff75de34e6106832117436b'], 'X-Forwarded-For': ['35.77.40.76, 64.252.167.142'], 'X-Forwarded-Port': ['443'], 'X-Forwarded-Proto': ['https']}, 'multiValueQueryStringParameters': None, 'path': '/user/12345/token', 'pathParameters': {'universal_id': '12345'}, 'queryStringParameters': None, 'requestContext': {'accountId': '020595591797', 'apiId': 'w7yk1fxfi3', 'authorizer': {'id': 'test1234', 'integrationLatency': 265, 'meta': 'context', 'principalId': 'abc123'}, 'domainName': 'xxxxxx.execute-api.ap-northeast-1.amazonaws.com', 'domainPrefix': 'xxxxxxx', 'extendedRequestId': 'WU0xpEdXtjMFsoQ=', 'httpMethod': 'GET', 'identity': {'accessKey': None, 'accountId': None, 'caller': None, 'cognitoAuthenticationProvider': None, 'cognitoAuthenticationType': None, 'cognitoIdentityId': None, 'cognitoIdentityPoolId': None, 'principalOrgId': None, 'sourceIp': '35.77.40.76', 'user': None, 'userAgent': 'curl/7.79.1', 'userArn': None}, 'path': '/prod/user/12345/token', 'protocol': 'HTTP/1.1', 'requestId': 'bbb0f5de-b6d5-449c-822a-xxxxxxxxx', 'requestTime': '04/Aug/2022:06:32:42 +0000', 'requestTimeEpoch': 1659594762398, 'resourceId': 'c4wl5g', 'resourcePath': '/user/{universal_id}/token', 'stage': 'prod'}, 'resource': '/user/{universal_id}/token', 'stageVariables': None }
デプロイと検証
デプロイをしてリソースを一括作成します
$ cdk deploy
curl でコマンドを入力して検証します
API の URL は API Gateway のコンソール画面から取得してください
$ curl -H 'Authorization:Bearer 123' https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/user/12345/token {"id": "01G9KSVXN50DBYYRQCEBE0RDMZ", "token": "token-12345"}
正常に返ってきました
次にオーソライザーの lambda ないで Deny となるようにリクエストします
$ curl -H 'Authorization:Bearer 111' https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/user/12345/token {"Message":"User is not authorized to access this resource with an explicit deny"}
今度はアクセスが拒否されました
最後に、Bearer と入れずにオーソラーザー処理前で拒否されるようにリクエストします
$ curl -H 'Authorization:111' https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/user/12345/token {"message":"Unauthorized"}
Unauthorized と返ってきました
Lambda オーソライザーのログの出力がなかったので、今度は Lambda を起動させずに拒否されたようです