Lambda AuthorizerがDenyを返すとき、APIのステータスコードを403ではなく、401で返す with OpenAPI

API Gatewayのゲートウェイレスポンスで、StatusCodeを変更します。
2022.09.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ログインして利用するWebアプリとAPIを開発しているとき、アクセストークンの認証失敗時、または、Lambda AuthorizerでDenyと判断することがあります。 このとき、デフォルトでは、ステータスコード403が返ります。 今回は、Lambda AuthorizerでDenyと判定したとき、APIのステータスコードを401にしてみます。

おすすめの方

  • Lambda Authorizerで認証失敗時、403以外のステータスコードを返したい方
  • AWS SAMとOpenAPIでAPI GatewayとLambdaをデプロイしたい方

Lambda Authorizerをデプロイする

sam init

sam init \
    --runtime python3.9 \
    --name Lambda-Authorizer-Status-Code-Sample \
    --app-template hello-world \
    --no-tracing \
    --package-type Zip

SAMテンプレート

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda-Authorizer-Status-Code-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-Status-Code-Sample-Stack/api.yaml

  # Lambda Authorizer
  AuthorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: authorizer.lambda_handler
      Runtime: python3.9
      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/*

  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.9
      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}

Outputs:
  HelloWorldApi:
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/hello"

OpenAPI

api.yaml

openapi: 3.0.1
info:
  title: sample api
  version: 1.0.0

# https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/supported-gateway-response-types.html
x-amazon-apigateway-gateway-responses:
  ACCESS_DENIED:
    statusCode: 401

paths:
  /hello:
    get:
      tags:
        - hello
      responses:
        200:
          $ref: "#/components/responses/200"
        401:
          $ref: "#/components/responses/401"
        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

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
    401:
      description: Unauthorized
    403:
      description: User or client is not authorized.
    500:
      description: Internal Server Error

Lambda Authorizer

今回は、常にDenyを返します。

authorizer.py

def lambda_handler(event, context):
    return {
        'principalId': '*',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': 'execute-api:Invoke',
                    'Effect': 'Deny',
                    'Resource': event['methodArn']
                }
            ]
        }
    }

Lambdaコード

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-Status-Code-Sample-Stack/api.yaml

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-deploy

sam deploy \
    --template-file packaged.yaml \
    --stack-name Lambda-Authorizer-Status-Code-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-Status-Code-Sample-Stack \
    --query 'Stacks[].Outputs'

Authorizationなし

401が返ってきました。

$ curl -D - https://aaa.execute-api.ap-northeast-1.amazonaws.com/dev/hello

HTTP/2 401 
...

{"message":"Unauthorized"}

Authorizationあり

何もしなければ403が返ってきますが、401が返ってきました。期待通りです。

curl -D - https://aaa.execute-api.ap-northeast-1.amazonaws.com/dev/hello \
  --header 'authorization: xxx'

HTTP/2 401 
...

{"message":"User is not authorized to access this resource with an explicit deny"}

さいごに

API Gatewayのゲートウェイレスポンスを利用して、ステータスコードを401に変更してみました。 API Gatewayのゲートウェイレスポンスは、ほかにもいろいろな種類や変更できる箇所があるので、必要に応じて活用したいです。

参考