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 を起動させずに拒否されたようです

無料相談実施中
AWSを使用したサーバーレスアプリケーションの構築
サーバーレス開発内製化、AWSへの移行等
様々な課題について無料でご相談お受けいたします。
最新情報をチェックしよう!