OASでLambda AuthorizerをREST APIにimportする

2020.06.05

API Gateway REST APIを作成する場合にLambda AuthorizerでAPIを保護するのはよくあるケースです.
またAPI定義をOAS (Open API Specification) で記載するのもよくあるケースです.
API GayewayのドキュメントではOAS 拡張でどのようにLambda Authorizerを記載しているかは書いてあるのですが, 実装をどのようにすべきかがいまいちわからなかったので試してみました.

write lambda function

まずはREST APIから呼び出すLambda Authorizerのための関数と統合バックエンドのための関数を書いていきます.
どちらの関数に関してもパッケージの初期処理と必要な依存関係を入れるために下記の処理を事前に行います.

$ yarn init -y
$ yarn add -D typescript @types/aws-lambda @types/node

Lambda Authorizer Function

まずはLambda Authorizerで利用する関数を書いていきます.
REST APIから流れてくるリクエストの種類としては大まかに2つに分けることができます. そして今回はトークンを利用してAPIを保護します.
Lambda Authorizerではないですが, Cognito User Poolを利用した保護も可能です.

  • Token based Lambda Authorizer: JWTやOAuthトークンなどのベアラトークンを受け取ってアクセスを制限するパターン
  • REQUEST based Lambda Authorizer: ヘッダーやクエリストリングなど経由でトークンを受け取ってアクセスを制御するパターン

REST APIからLambda Authorizerにトークンを利用する場合に渡される情報は下記のようになります.
typeはそのままどの方法かで, authorizationTokenがクライアントがAPIに対して渡したトークンになります.
なのでLambda AuthorizerではauthorizationTokenで渡されたトークンを検証することでAPIの保護を実現します.
最後にmethodArnですが, REST APIが実行したいメソッドのARNになります.
ここだけでは頭にはてなが100個くらい浮かびますが, Lambda Authorizer Functionの出力を見れば納得できますのでいったんはそのはてなをコインに変えてください.

{
  "type":"TOKEN",
  "authorizationToken":"{caller-supplied-token}",
  "methodArn":"arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]"
}

次にLambda AuthorizerがREST APIに対して返すべきレスポンスをみていきます.
レスポンスで最も重要なのは統合バックエンドのLambda関数呼び出しを許可または拒否するポリシードキュメントの部分です.
REST APIからのリクエストにmethodArnが含まれていたのはポリシードキュメント生成で必要になるためです.
principalIdはリクエストのクライアントを一意にし, contextの情報を統合バックエンドにcontextとして渡すために利用します.
ユーザ情報などcontextで渡すことで処理をしたりできますね. usageIdentifierKeyは名前の通りAPI ステージの使用量プランのAPIキーを指します.

{
  "principalId": "yyyyyyyy",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow|Deny",
        "Resource": "arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]"
      }
    ]
  },
  "context": {
    "stringKey": "value",
    "numberKey": "1",
    "booleanKey": "true"
  },
  "usageIdentifierKey": "{api-key}"
}

実際にLambda Functionを実装していきます.
今回はクライアントからTokenが送られてきてかつ, 「super-super-secure-token」というとても強固で誰も見破ることができ無いであろうトークンだった場合にはバックエンドのLambda関数を呼び出します.
そうでない場合にはアクセスを拒否します.

import {APIGatewayAuthorizerEvent,  APIGatewayAuthorizerResult} from 'aws-lambda'

async function handler(event: APIGatewayAuthorizerEvent): Promise<APIGatewayAuthorizerResult> {
  if (event.type !== 'TOKEN') {
    return {
      principalId: null,
      policyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Action: 'execute-api:Invoke',
            Effect: 'Deny',
            Resource: event.methodArn,
          }
        ]
      },
    }
  }
  console.log(`handle even\ntype: ${event.type}\ntoken: ${event.authorizationToken}`)

  if (event.authorizationToken === 'super-super-secure-token') {
    return {
      principalId: 'super-secure-boy',
      policyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Action: 'execute-api:Invoke',
            Effect: 'Allow',
            Resource: event.methodArn,
          }
        ]
      },
    }
  }
  return {
    principalId: null,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: 'Deny',
          Resource: event.methodArn,
        }
      ]
    },
  }
}

export {
  handler
}

とても簡単な実装ではありますが, アクセスを拒否したい場合は「Deny」を持ったポリシーを, そうでない場合は「Allow」を持ったポリシーを返します.
最後に関数をデプロイします.

# build handler
$ yarn run tsc index.ts
$ zip index.zip index.js

$ aws --region us-east-1 lambda create-function \
  --function-name sdx_authorizer \
  --runtime nodejs12.x \
  --role arn:aws:iam::123456789012:role/lambda-role \
  --handler index.handler \
  --zip-file fileb://index.zip

Backend Lambda Function

バックエンドのLambda関数に関しては通常の統合バックエンドで利用するLambda関数を作成すれば問題ありません.
私たちがとてもセキュアなAPIを用意して守りたかった内容はバンドメンバーとパート一覧です. 間違いありませんよね?
私はそう思っています.

import {Context, APIGatewayProxyEvent, APIGatewayProxyResult} from 'aws-lambda'

async function handler(event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> {
  const functionName = context.functionName
  const version = context.functionVersion
  console.log(`function: ${functionName}\nversion: ${version}`)

  const method = event.httpMethod
  const body = event.body
  console.log(`method: ${method}\nbody: ${body}`)

  const members = {
    members: [
      {
        id: 1,
        name: 'Anthony Kiedis',
        part: 'vocal',
      },
      {
        id: 2,
        name: 'Chad Smith',
        part: 'drum'
      },
      {
        id: 3,
        name: 'Flea',
        part: 'bass guitar',
      },
      {
        id: 4,
        name: 'John Frusciante',
        part: 'guitar',
      },
    ]
  }

  return {
    body: JSON.stringify(members),
    headers: {
      'Content-Type': 'application/json'
    },
    statusCode: 200,
    isBase64Encoded: false,
  }
}

export {
  handler
}

特に解説することもないのでデプロイしていきます.

# build handler
$ yarn run tsc index.ts
$ zip index.zip index.js

$ aws --region us-east-1 lambda create-function \
  --function-name sdx_handler \
  --runtime nodejs12.x \
  --role arn:aws:iam::123456789012:role/lambda-role \
  --handler index.handler \
  --zip-file fileb://index.zip

これでLambda Functionsの準備は完了しました. 次にすべきはそうですね, OASを書いていきます.

Write OAS

つぎにOASを書いていきます.
基本的にはOAS通りに書いていき, securitySchemes部分にLambda Authorizerの設定を書いていきます.
今回はsecuritySchemesを利用するのでpathにsecurityを追加します.
今回はScopeでの絞り込みはしないため値には空の配列を渡します.

paths:
  /members:
    get:
      summary: lists all members
      security:
        - sdx_authorizer: []

ちょっとだけ飛ばして, componentsの中身をみていきます.
ここにLambda Authorizerで必要な情報を定義していきます. OASのSecuritySchemasで必要な項目に加えて, 「x-amazon-apigateway-authtype」と「x-amazon-apigateway-authorizer」を指定します. 今回はヘッダでtokenを受け取ってAPIの保護を行うので下記のように設定を行います.

components:
  securitySchemes:
    sdx_authorizer:
      type: apiKey
      description: Lambda Authorizer
      name: Authorization
      in: header
      x-amazon-apigateway-authtype: custom
      x-amazon-apigateway-authorizer:
        authorizerUri: arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:sdx_authorizer/invocations
        authorizerResultTtlInSeconds: 300
        type: "token"

全体を通した定義は下記のようになります.

openapi: '3.0.2'
info:
  title: super secure web API
  description: Lambda Authorizer Sample
  version: '1.0'
servers:
- url: "https://{id}.execute-api.us-east-1.amazonaws.com/{basePath}"
  variables:
    basePath:
      default: /v1
    id:
      default: xxxxxxxxx
paths:
  /members:
    get:
      summary: lists all members
      security:
        - sdx_authorizer: []
      responses:
        200:
          description: "200 response"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Member"
              examples:
                jsonObject:
                  summary: A Sample Member Object
                  $ref: '#/components/examples/memberExample'
      x-amazon-apigateway-integration:
        type: aws_proxy
        httpMethod: POST
        passthroughBehavior: when_no_match
        uri:  arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:sdx_basic/invocations
        responses:
          default:
            statusCode: '200'

security:
  - sdx_authorizer: []

components:
  securitySchemes:
    sdx_authorizer:
      type: apiKey
      description: Lambda Authorizer
      name: Authorization
      in: header
      x-amazon-apigateway-authtype: custom
      x-amazon-apigateway-authorizer:
        authorizerUri: arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:sdx_authorizer/invocations
        authorizerResultTtlInSeconds: 300
        type: "token"
  schemas:
    Empty:
      title: "Empty Schema"
      type: "object"
    Member:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        part:
          type: string
  examples:
    memberExample:
      value:
        id: 1
        name: dave navarro
        part: guitar

これでOASとLambda Functionsが揃いました.
あとはデプロイするだけです.

create API

REST APIを作成します.

$ aws --region us-east-1 apigateway import-rest-api \
  --fail-on-warnings \
  --body file://define.yaml

{
    "id": "xxxxxxxxxx",
    "name": "super secure web API",
    "description": "Lambda Authorizer Sample",
    "createdDate": 1591338069,
    "version": "1.0",
    "apiKeySource": "HEADER",
    "endpointConfiguration": {
        "types": [
            "EDGE"
        ]
    }
}

次にLambdaのリソースポリシーでAPI GatewayからのLambda関数の発火を許可します.

$ aws --region us-east-1 lambda add-permission \
  --function-name sdx_basic \
  --action lambda:InvokeFunction \
  --statement-id super_secure_backend \
  --principal apigateway.amazonaws.com \
  --source-arn 'arn:aws:execute-api:us-east-1:123456789012:xxxxxxxxxx/*/*/*'

$ aws --region us-east-1 lambda add-permission \
  --function-name sdx_authorizer \
  --action lambda:InvokeFunction \
  --statement-id super_secure_authorizer \
  --principal apigateway.amazonaws.com \
  --source-arn 'arn:aws:execute-api:us-east-1:123456789012:xxxxxxxxxx/authorizers/*'

最後にAPI Gatewayをデプロイします.

$ aws --region us-east-1 apigateway create-deployment \
  --rest-api-id xxxxxxxxxx \
  --stage-name v1
  
{
    "id": "xxxxx",
    "createdDate": 1591338284
}



今までの設定でとても強固に守られたAPIにアクセスしてみます.


$ curl https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/v1/members

{"message":"Unauthorized"}

$ curl -H "Authorization: super-super-secure-token" https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/v1/members

{"members":[{"id":1,"name":"Anthony Kiedis","part":"vocal"},{"id":2,"name":"Chad Smith","part":"drum"},{"id":3,"name":"Flea","part":"bass guitar"},{"id":4,"name":"John Frusciante","part":"guitar"}]}

とても強固に守られていますね.

To close

Lambda Authorizerは用語が多いことやコンポーネントが多いのでこんがらがりやすいですが,

  • バックエンドの呼び出しを保護しかつ, ある程度保護とバックエンドの実行を疎にする
  • Lambda AuthorizerはAPI Gateway経由でLambda 関数を呼び出して, ポリシードキュメントを受け取って評価する

このことを意識していれば大体は理解できると思います. その上でのインタフェースが今回記載したOASになります.
この記事がお役立ちましたら幸いです.

References