[OpenAPI] AWS SAMでLambdaオーソライザーを「適用するLambda」と「適用しないAPI」を作ってみた

AWS SAMとOpenAPIでLambdaオーソライザーを適用したAPIと適用しないAPIを作ってみました。
2021.11.04

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オーソライザーの設定もあります。

api.yaml

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を作成できないからです。

template.yaml

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

authorizationabcならOKとしています。

authorizer.py

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と共通です。

app.py

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

api.yaml

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テンプレート

template.yaml

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オーソライザーの記述を移動する部分で地味に苦労しました。

どなたかの参考になれば幸いです。

参考