はじめに
API Gateway を使うとインターネット上に REST API を公開できます。インターネット上に公開する際に、特定のユーザーやシステムにのみアクセスを制限させたい場合があります。そういったときには、API Gateway の認証機能が便利に使えます。
API Gateway の認証機能にはいくつか種類があります。
- リソースポリシー
- IAM アクセス許可
- Lambda オーソライザー
- Cognito オーソライザー
それぞれの詳細が気になる方は、次の AWS Document をチェックしてみてください。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-control-access-to-api.html
今回は、Lambda オーソライザーを触っていきたいと思います。Lambda オーソライザーを使うことで、API Gateway に独自の認証機能を付与することが出来ます。
Lambda オーソライザーのざっくりイメージはこんな感じです。
独自の認証ロジックを自分たちで作り上げることが出来るので、柔軟な認証機能を提供できます。Lambda としてプログラムが動くため、背後にある DynamoDB や RDS などのデータストアと連携も出来ます。
記事を作成するにあたり、参考 URL に記載させていただいている記事を参考にしました。(ありがとうございます!!!)
SAM を使って作成
今回の記事では、AWS Serverless Application Model (SAM) を使って、API Gateway と Lambda オーソライザー、Lambda 関数を作っていきます。
sam init
コマンドで、新しいプロジェクトを作成します。
sam init \
--runtime python3.9 \
--name Lambda-Authorizer-Sample \
--app-template hello-world \
--package-type Zip
SAM の template.yaml
を指定します。ポイントは次の通りです
- API Gateway にある
Auth
で、Lambda オーソライザーに関する指定をする -
AuthorizerFunction
で、Lambda オーソライザーを定義する。Python のファイル名をauthorizer
とする -
HelloWorldFunction
で、呼び出したい Lambda 関数を定義する。Python のファイル名をapp
とする
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
Lambda-Authorizer-Sample
Sample SAM Template for Lambda-Authorizer-Sample
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 30
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: dev
MethodSettings:
- ResourcePath: /
HttpMethod: GET
Auth:
DefaultAuthorizer: MyLambdaAuthorizer
Authorizers:
MyLambdaAuthorizer:
FunctionArn: !GetAtt AuthorizerFunction.Arn
Identity:
ReauthorizeEvery: 0
# Lambda Authorizer
AuthorizerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: authorizer.lambda_handler
Runtime: python3.9
Timeout: 30
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hello
Method: get
RestApiId: !Ref MyApi
Outputs:
HelloWorldApi:
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
Lambda 関数の Python プログラムを次のように作成します。
app.py
- 実際に動すための Lambda 関数
- hello world を出力する単純なもの
import json
from logging import getLogger, INFO
logger = getLogger(__name__)
logger.setLevel(INFO)
def lambda_handler(event, context):
print("============ event の出力 ============")
logger.info(json.dumps(event))
return {
'statusCode': 200,
'body': json.dumps({
'message': 'hello world',
}),
}
authorizer.py
- Lambda オーソライザー。ユーザーからのリクエストに含まれる HTTP ヘッダーからトークンを抜き出して、トークンが正当なものか確認する。
- HTTP ヘッダーに埋め込まれたトークンは、
event['authorizationToken']
で取得可能 - この Python プログラムでは、トークンが
abc
だったら正当なものと判断するようにハードコーディングされている。実際のプロダクトでは、データストアなどに問い合わせるようなロジックを入れても良い。 -
policyDocument
のEffect
の文字列がAllow
で Return すると、認証が通ったことを表現している。Deny
は認証が通らなかったことを表現している。
import json
from logging import getLogger, INFO
logger = getLogger(__name__)
logger.setLevel(INFO)
def lambda_handler(event, context):
print("============ event の出力 ============")
logger.info(json.dumps(event))
token = event['authorizationToken']
effect = 'Deny'
if token == 'abc':
effect = 'Allow'
return {
'principalId': '*',
'policyDocument': {
'Version': '2012-10-17',
'Statement': [
{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': event['methodArn']
}
]
}
}
それでは、SAM で Build を行った後に、AWS 上にでデプロイをします。
cd Lambda-Authorizer-Sample/
sam build
sam deploy --guided
SAM Stack の名前など、適当に指定します。
Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]: lambda-authorizer-stack
AWS Region [ap-northeast-1]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]:
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]:
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]:
HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: Y
Save arguments to configuration file [Y/n]:
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:
SAM Deploy の結果、API Gateway と、Lambda オーソライザー、Lambda 関数がデプロイされています。
API Gateway の Lambda Authorizer はこのような設定になっています。
- Token Source が
Authorization
となっている。API Gateway にリクエストするときに渡すトークンは、Authorization
のキーで渡す必要がある。
Lambda オーソライザーの動作確認
まずは、なにもトークンを渡さない状態で、API Gateway を実行してみます。すると、Unauthorized
と返ってきました。Lambda オーソライザーによって、アクセスが拒否されました。
> curl https://hyt7k1r2o7.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"Unauthorized"}
次に、実際にリクエストでトークンを渡してみます。--header 'authorization: abc'
が大事なポイントです。
次の実行結果のように、正常に Lambda 関数の実行が出来ました。
> curl https://hyt7k1r2o7.execute-api.ap-northeast-1.amazonaws.com/dev/hello \
--header 'authorization: abc'
{"message": "hello world"}
このときに、Lambda オーソライザーの関数が受け取るイベントの中身は次の通りです。
-
"authorizationToken": "abc"
の部分で、トークンを受け取っていることがわかります。
{
"type": "TOKEN",
"methodArn": "arn:aws:execute-api:ap-northeast-1:xxxxxxxxxxxx:hyt7k1r2o7/dev/GET/hello",
"authorizationToken": "abc"
}
また、後続の Lambda 関数が受け取るイベントです。(Lambda オーソライザーの次に呼び出される Lambda 関数)
{
"resource": "/hello",
"path": "/hello",
"httpMethod": "GET",
"headers": {
"Accept": "*/*",
"Authorization": "abc",
"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-Country": "JP",
"Host": "hyt7k1r2o7.execute-api.ap-northeast-1.amazonaws.com",
"User-Agent": "curl/7.79.1",
"Via": "2.0 f79910dd066cb79d5b224ab3f88841e4.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "xxBlqNZHhj7XXRVc7-QPmmnFoubexBFWcZMTtxc-c7krIaEeBzW6qQ==",
"X-Amzn-Trace-Id": "Root=1-629b1028-35cb88da516a260f5063e428",
"X-Forwarded-For": "35.75.xx.xx, 130.176.xx.xx",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"multiValueHeaders": {
"Accept": [
"*/*"
],
"Authorization": [
"abc"
],
"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-Country": [
"JP"
],
"Host": [
"hyt7k1r2o7.execute-api.ap-northeast-1.amazonaws.com"
],
"User-Agent": [
"curl/7.79.1"
],
"Via": [
"2.0 f79910dd066cb79d5b224ab3f88841e4.cloudfront.net (CloudFront)"
],
"X-Amz-Cf-Id": [
"xxBlqNZHhj7XXRVc7-QPmmnFoubexBFWcZMTtxc-c7krIaEeBzW6qQ=="
],
"X-Amzn-Trace-Id": [
"Root=1-629b1028-35cb88da516a260f5063e428"
],
"X-Forwarded-For": [
"35.75.xx.xx, 130.176.xx.xx"
],
"X-Forwarded-Port": [
"443"
],
"X-Forwarded-Proto": [
"https"
]
},
"queryStringParameters": null,
"multiValueQueryStringParameters": null,
"pathParameters": null,
"stageVariables": null,
"requestContext": {
"resourceId": "7zv8eo",
"authorizer": {
"principalId": "*",
"integrationLatency": 259
},
"resourcePath": "/hello",
"httpMethod": "GET",
"extendedRequestId": "TL92UG8TtjMFy9w=",
"requestTime": "04/Jun/2022:07:56:24 +0000",
"path": "/dev/hello",
"accountId": "xxxxxxxxxxxx",
"protocol": "HTTP/1.1",
"stage": "dev",
"domainPrefix": "hyt7k1r2o7",
"requestTimeEpoch": 1654329384284,
"requestId": "0854995a-b772-4645-a725-462e6e93e2ee",
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"sourceIp": "35.75.xx.xx",
"principalOrgId": null,
"accessKey": null,
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "curl/7.79.1",
"user": null
},
"domainName": "hyt7k1r2o7.execute-api.ap-northeast-1.amazonaws.com",
"apiId": "hyt7k1r2o7"
},
"body": null,
"isBase64Encoded": false
}
オーソライザーから、本来の Lambda 関数にデータを渡す
Lambda オーソライザーで認証が通ったあとに、本来動かしたい Lambda 関数にデータを渡したいときがあります。例えば、Lambda オーソライザーで、渡されたトークンを基にユーザーやグループを特定した後に、その情報を後続の Lambda 関数に渡したい状況があります。
後続の Lambda 関数にデータを渡す方法を確認していきましょう。
Lambda オーソライザーのプログラムを、次のように変更します。
- return しているデータに含まれている
context
に、watasu_data
という名前でデータを渡す。この名前は好きな名前で問題ない
import json
import base64
from logging import getLogger, INFO
logger = getLogger(__name__)
logger.setLevel(INFO)
def lambda_handler(event, context):
print("============ event の出力 ============")
logger.info(json.dumps(event))
token = event['authorizationToken']
effect = 'Deny'
context = {}
if token == 'abc':
effect = 'Allow'
context = {
"parent1": "val1",
"parent2": {
"child1": "val1",
"child2": "val2",
}
}
json_context = json.dumps(context)
base64_context = base64.b64encode(json_context.encode('utf8'))
return {
'principalId': '*',
'policyDocument': {
'Version': '2012-10-17',
'Statement': [
{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': event['methodArn']
}
]
},
'context': {
'watasu_data': base64_context.decode('utf-8')
}
}
これによって、本来呼びだしたい Lambda 関数側は次のようなイベントを受け取ることが出来ます。
{
"resource": "/hello",
"path": "/hello",
"httpMethod": "GET",
"headers": {
"Accept": "*/*",
"Authorization": "abc",
"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-Country": "JP",
"Host": "hyt7k1r2o7.execute-api.ap-northeast-1.amazonaws.com",
"User-Agent": "curl/7.79.1",
"Via": "2.0 3793d7fea64206c86c6da516357453b6.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "qqoD8clhvMHNK3S4flpZqITRccibyNEL-54vWIrf3WjDeHJeCiS3jQ==",
"X-Amzn-Trace-Id": "Root=1-629b1753-6374f54037aaf34f25cad3e1",
"X-Forwarded-For": "35.75.xx.xx, 64.252.172.142",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"multiValueHeaders": {
"Accept": [
"*/*"
],
"Authorization": [
"abc"
],
"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-Country": [
"JP"
],
"Host": [
"hyt7k1r2o7.execute-api.ap-northeast-1.amazonaws.com"
],
"User-Agent": [
"curl/7.79.1"
],
"Via": [
"2.0 3793d7fea64206c86c6da516357453b6.cloudfront.net (CloudFront)"
],
"X-Amz-Cf-Id": [
"qqoD8clhvMHNK3S4flpZqITRccibyNEL-54vWIrf3WjDeHJeCiS3jQ=="
],
"X-Amzn-Trace-Id": [
"Root=1-629b1753-6374f54037aaf34f25cad3e1"
],
"X-Forwarded-For": [
"35.75.xx.xx, 64.252.xx.xx"
],
"X-Forwarded-Port": [
"443"
],
"X-Forwarded-Proto": [
"https"
]
},
"queryStringParameters": null,
"multiValueQueryStringParameters": null,
"pathParameters": null,
"stageVariables": null,
"requestContext": {
"resourceId": "7zv8eo",
"authorizer": {
"watasu_data": "eyJwYXJlbnQxIjogInZhbDEiLCAicGFyZW50MiI6IHsiY2hpbGQxIjogInZhbDEiLCAiY2hpbGQyIjogInZhbDIifX0=",
"principalId": "*",
"integrationLatency": 21
},
"resourcePath": "/hello",
"httpMethod": "GET",
"extendedRequestId": "TMCVFH6UNjMFa_Q=",
"requestTime": "04/Jun/2022:08:26:59 +0000",
"path": "/dev/hello",
"accountId": "xxxxxxxxxxxx",
"protocol": "HTTP/1.1",
"stage": "dev",
"domainPrefix": "hyt7k1r2o7",
"requestTimeEpoch": 1654331219561,
"requestId": "869bfa65-de36-4ee4-b2b8-074b70cfeb20",
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"sourceIp": "35.75.xx.xx",
"principalOrgId": null,
"accessKey": null,
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "curl/7.79.1",
"user": null
},
"domainName": "hyt7k1r2o7.execute-api.ap-northeast-1.amazonaws.com",
"apiId": "hyt7k1r2o7"
},
"body": null,
"isBase64Encoded": false
}
大事な部分をピックアップすると、次の部分です。
-
authorizer
に、watasu_data
が格納されており、Base64 でエンコードされたデータが入っています。
"requestContext": {
"resourceId": "7zv8eo",
"authorizer": {
"watasu_data": "eyJwYXJlbnQxIjogInZhbDEiLCAicGFyZW50MiI6IHsiY2hpbGQxIjogInZhbDEiLCAiY2hpbGQyIjogInZhbDIifX0=",
"principalId": "*",
"integrationLatency": 21
},
この文字をデコードしてみると、Lambda オーソライザーで渡したデータを、正しく受け取れていることがわかります。
> echo -n "eyJwYXJlbnQxIjogInZhbDEiLCAicGFyZW50MiI6IHsiY2hpbGQxIjogInZhbDEiLCAiY2hpbGQyIjogInZhbDIifX0=" | base64 -d
{"parent1": "val1", "parent2": {"child1": "val1", "child2": "val2"}}⏎
検証を通じてわかったこと
- Lambda オーソライザーを使うことで、独自の認証ロジックを API Gateway に設定することが出来る。
- Lambda オーソライザーが、Return するときの文字列で、認証が通った or 通らなかった を指定できる。
-
policyDocument
のEffect
の文字列がAllow
で Return すると、認証が通ったことを表現している。Deny
は認証が通らなかったことを表現している。
-
- Lambda オーソライザー から、本体の Lambda 関数にデータを渡すことが出来る。
- return の時に、context にデータを渡すと、本体の Lambda がデータを受け取ることが可能。
- context に渡すデータのキー値は、好きな値を指定して良い
参考 URL
API Gateway Lambda オーソライザーを使用する
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
API GatewayのLambda オーソライザーから後続のLambdaにデータを引き渡す
https://dev.classmethod.jp/articles/lambda-authorizer/
AWS SAMでLambdaオーソライザーを「適用するAPI」と「適用しないAPI」を作ってみた
https://dev.classmethod.jp/articles/aws-sam-api-gateway-lambda-authorizer/
API Gateway LambdaオーソライザーでJWTトークン認証をやってみる
https://www.blog.danishi.net/2021/08/19/post-5336/