[OpenAPI] AWS SAMでLambdaオーソライザーを「適用するLambda」と「適用しないAPI」を作ってみた
CognitoやAuth0を使って、API GatewayのLambdaオーソライザーで「OK・NG」を判断することは多いと思います。 このとき、一部のAPIを公開したくなったので、試してみました。
以前はAWS SAMだけで完結していましたが、本記事では、OpenAPIを使っています。
おすすめの方
- AWS SAMでOpenAPIを使ってAPIを作りたい方
- AWS SAMでLambdaオーソライザーを作りたい方
- AWS SAMで一部のAPI(Lambda)にLambdaオーソライザーを適用したくない方
サーバーレスアプリを作成する
sam init
sam init \ --runtime python3.8 \ --name Lambda-Authorizer-Sample \ --app-template hello-world \ --package-type Zip
OpenAPI定義
Lambdaオーソライザーの設定もあります。
openapi: 3.0.1 info: title: sample api version: 1.0.0 paths: /hello: get: tags: - hello responses: 200: $ref: "#/components/responses/200" 403: $ref: "#/components/responses/403" 500: $ref: "#/components/responses/500" security: - MyLambdaAuthorizer: [] x-amazon-apigateway-integration: type: aws_proxy uri: 'Fn::Sub': >- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations httpMethod: POST responses: default: statusCode: 200 passthroughBehavior: when_no_templates contentHandling: CONVERT_TO_TEXT /hello2: get: tags: - hello responses: 200: $ref: "#/components/responses/200" 500: $ref: "#/components/responses/500" x-amazon-apigateway-integration: type: aws_proxy uri: 'Fn::Sub': >- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorld2Function.Arn}/invocations httpMethod: POST responses: default: statusCode: 200 passthroughBehavior: when_no_templates contentHandling: CONVERT_TO_TEXT components: securitySchemes: MyLambdaAuthorizer: type: apiKey name: Authorization in: header x-amazon-apigateway-authtype: custom x-amazon-apigateway-authorizer: authorizerUri: 'Fn::Sub': >- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizerFunction.Arn}/invocations authorizerResultTtlInSeconds: 0 type: token responses: 200: description: Success 403: description: User or client is not authorized. 500: description: Internal Server Error
SAMテンプレート
Lambda自体は3つ作成します。
- Lambdaオーソライザー
GET /hello
用(Authorizerあり)GET /hello2
用(Authorizerなし)
また、API GatewayがLambdaを呼び出す権限(AWS::Lambda::Permission
)も作成します。
- Lambdaオーソライザー用のPermission
- 自分で作成する(★これ)
GET /hello
用のPermission- AWS SAMが自動で作成してくれる
GET /hello2
用のPermission- AWS SAMが自動で作成してくれる
これは、Lambda(Lambdaオーソライザー)にEvents
の定義がないため、AWS SAMがPermissionを作成できないからです。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Lambda-Authorizer-Sample Resources: MyApi: Type: AWS::Serverless::Api Properties: StageName: dev DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: s3://cm-fujii.genki-deploy/Lambda-Authorizer-Sample-Stack/api.yaml # Lambda Authorizer AuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: authorizer.lambda_handler Runtime: python3.8 Timeout: 5 AuthorizerFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${AuthorizerFunction} AuthorizerFunctionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt AuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyApi}/authorizers/* # API: GET /hello (Authorizerあり) HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.8 Timeout: 5 Events: HelloWorld: Type: Api Properties: Path: /hello Method: get RestApiId: !Ref MyApi HelloWorldFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${HelloWorldFunction} # API: GET /hello2 (Authorizerなし) HelloWorld2Function: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.8 Timeout: 5 Events: HelloWorld: Type: Api Properties: Path: /hello2 Method: get RestApiId: !Ref MyApi HelloWorld2FunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${HelloWorld2Function} Outputs: HelloWorldApi: Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/hello"
Lambdaコード
Authorizer
authorization
がabc
ならOKとしています。
def lambda_handler(event, context): 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'] } ] } }
GET /hello
用
GET /hello2
と共通です。
import json def lambda_handler(event, context): return { 'statusCode': 200, 'body': json.dumps({ 'message': 'hello world', }), }
デプロイ
aws s3 cp \ api.yaml \ s3://cm-fujii.genki-deploy/Lambda-Authorizer-Sample-Stack/api.yaml sam build sam package \ --output-template-file packaged.yaml \ --s3-bucket cm-fujii.genki-deploy sam deploy \ --template-file packaged.yaml \ --stack-name Lambda-Authorizer-Sample-Stack \ --s3-bucket cm-fujii.genki-deploy \ --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset
動作確認
APIエンドポイントを取得する
aws cloudformation describe-stacks \ --stack-name Lambda-Authorizer-Sample-Stack \ --query 'Stacks[].Outputs'
GET /hello
Authorizationあり(OK)
curl https://xxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello \ --header 'authorization: abc' {"message": "hello world"}
Authorizationあり(NG)
curl https://xxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello \ --header 'authorization: xxx' {"Message":"User is not authorized to access this resource with an explicit deny"}
Authorizationなし(NG)
curl https://xxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello {"message":"Unauthorized"}
GET /hello2
Authorizationなし(OK)
curl https://xxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello2 {"message": "hello world"}
おまけ:すべてにLambdaオーソライザーを適用する場合
すべてのAPIにLambdaオーソライザーを適用する場合は、OpenAPI側ではなく、AWS SAMテンプレート側に定義を記載することも可能です。 ただし、Swagger等で確認したときにAuthorizerに関する情報は見れません。OpenAPI側にsecurityの記載が無いからです。
OpenAPI
openapi: 3.0.1 info: title: sample api version: 1.0.0 paths: /hello: get: tags: - "hello" responses: 200: $ref: "#/components/responses/200" 403: $ref: "#/components/responses/403" 500: $ref: "#/components/responses/500" x-amazon-apigateway-integration: type: "aws_proxy" uri: 'Fn::Sub': >- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations httpMethod: "POST" responses: default: statusCode: "200" passthroughBehavior: "when_no_templates" contentHandling: "CONVERT_TO_TEXT" /hello2: get: tags: - "hello" responses: 200: $ref: "#/components/responses/200" 500: $ref: "#/components/responses/500" x-amazon-apigateway-integration: type: "aws_proxy" uri: 'Fn::Sub': >- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorld2Function.Arn}/invocations httpMethod: "POST" responses: default: statusCode: "200" passthroughBehavior: "when_no_templates" contentHandling: "CONVERT_TO_TEXT" components: responses: 200: description: "Success" 403: description: "User or client is not authorized." 500: description: "Internal Server Error"
SAMテンプレート
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Lambda-Authorizer-Sample Resources: MyApi: Type: AWS::Serverless::Api Properties: StageName: dev Auth: DefaultAuthorizer: MyLambdaAuthorizer Authorizers: MyLambdaAuthorizer: FunctionArn: !GetAtt AuthorizerFunction.Arn Identity: ReauthorizeEvery: 0 DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: s3://cm-fujii.genki-deploy/Lambda-Authorizer-Sample-Stack/api.yaml AuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: authorizer.lambda_handler Runtime: python3.8 Timeout: 5 AuthorizerFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${AuthorizerFunction} HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.8 Timeout: 5 Events: HelloWorld: Type: Api Properties: Path: /hello Method: get RestApiId: !Ref MyApi HelloWorldFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${HelloWorldFunction} HelloWorld2Function: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.8 Timeout: 5 Events: HelloWorld: Type: Api Properties: Path: /hello2 Method: get RestApiId: !Ref MyApi HelloWorld2FunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${HelloWorld2Function} Outputs: HelloWorldApi: Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/hello"
さいごに
「AWS SAM + OpenAPI」でAWS SAM側にLambdaオーソライザーの記述をしている状態から、一部だけオーソライザーを適用外にしたかったのですが、OpenAPI側にLambdaオーソライザーの記述を移動する部分で地味に苦労しました。
どなたかの参考になれば幸いです。