LoginSignup
32
19

More than 1 year has passed since last update.

API Gateway の Lambda オーソライザーをやってみた

Last updated at Posted at 2022-06-04

はじめに

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 オーソライザーのざっくりイメージはこんな感じです。

image-20220604182004368.png

独自の認証ロジックを自分たちで作り上げることが出来るので、柔軟な認証機能を提供できます。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 プログラムを次のように作成します。

image-20220604183408666.png

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 だったら正当なものと判断するようにハードコーディングされている。実際のプロダクトでは、データストアなどに問い合わせるようなロジックを入れても良い。
  • policyDocumentEffect の文字列が 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 のキーで渡す必要がある。

image-20220604164920931.png

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 通らなかった を指定できる。
    • policyDocumentEffect の文字列が 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/

32
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
19