今回やること
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 を起動させずに拒否されたようです