API Gateway の Lambda Authorizer をRuby で実装してみる

## 概要

API Gateway の機能である Lambda Authorizer を Ruby で実装してみました。
オーソライザーの中身は AWS Cognito を利用したトークンベースで行っています。

デプロイには [Serverless](https://serverless.com/) を使用しました。
コードは[こちら](https://github.com/nightswinger/cognito-custom-authorizers)でも公開しています。

## Lambda Authorizer とは

Lambda 関数を使用してAPIへのアクセスを制御する API Gateway の機能です。
[API Gateway Lambda オーソライザーの使用](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html)

## Authorizer 関数の実装

ここでは Cognito ユーザープールのトークンを使ってクライアントを認可しています。
リクエストヘッダーに含まれるベアラートークンを検証し、成功すればIAMポリシーを返してAPIへのアクセスを許可します。

```ruby:auth.rb
require 'jwt'
require 'open-uri'

ISS = "https://cognito-idp.#{ENV['COGNITO_USER_POOL_ID'].gsub(/(?<=\d)(.*)/, '')}.amazonaws.com/#{ENV['COGNITO_USER_POOL_ID']}"

def authorize(event:, context:)
  puts 'Auth function invoked'

  token = event['authorizationToken'][7..-1]
  header = Base64.urlsafe_decode64(token.split('.')[0])
  kid = JSON.parse(header, symbolize_names: true)[:kid]

  res = OpenURI.open_uri("#{ISS}/.well-known/jwks.json")
  keys = JSON.parse(res.read, symbolize_names: true)[:keys]

  key = keys.find { |k| k[:kid] == kid }
  pem = JWT::JWK.import(key).public_key

  begin
    decoded = JWT.decode token, pem, true, verify_iat: true, iss: ISS, verify_iss: true, algorithm: 'RS256'
  rescue JWT::JWKError => e
    puts "Provided JWKs is invalid: #{e}"
    return generate_policy(nil, 'Deny', event['methodArn'])
  rescue JWT::DecodeError => e
    puts "Failed to authorize: #{e}"
    return generate_policy(nil, 'Deny', event['methodArn'])
  end

  generate_policy(decoded[0]['sub'], 'Allow', event['methodArn'])
end

def generate_policy(principal_id, effect, resource)
  auth_response = { principalId: principal_id }
  auth_response[:policyDocument] = {
    Version: '2012-10-17',
    Statement: [
      { Action: 'execute-api:Invoke', Effect: effect, Resource: resource }
    ]
  }
  auth_response
end
```

## 認可後に実行される Lambda 関数の実装

```ruby:handler.rb
def private_endpoint(event:, context:)
  { statusCode: 200, body: 'Only logged in users can see this' }
end
```

## デプロイする

APIの構築に必要なAWSのリソースを定義したファイルを作成します。
8行目の`COGNITO_USER_POOL_ID`の値を実際のユーザープールIDに置き換える必要があります。

`privateEndpoint`の`events`にある`authorizer`でこの関数が実行される前に呼び出される Lambda Authorizer の設定を行なっています。
Serverlessフレームワークを使えば簡単にこうした設定をすることができます。

```yaml:serverless.yml
service: aws-ruby-cognito-custom-authorizers

provider:
  name: aws
  runtime: ruby2.5

  environment:
    COGNITO_USER_POOL_ID: COGNITO_USER_POOL_ID

plugins:
  - serverless-hooks-plugin

custom:
  hooks:
    package:initialize:
      - bundle install --deployment

functions:
  auth:
    handler: auth.authorize
  privateEndpoint:
    handler: handler.private_endpoint
    events:
      - http:
          path: api/private
          method: get
          authorizer: auth
          cors:
            origins:
              - '*'
            headers:
              - Content-Type
              - X-Amz-Date
              - Authorization
              - X-Api-Key
              - X-Amz-Security-Token
```

Gemfileを作成し、必要なライブラリをインストールします。

```bash
bundle init
echo \"jwt\" >> Gemfile
bundle
```

デプロイするには以下のコマンドを実行します。

```bash
npm install -g serverless
npm install --save serverless-hooks-plugin
sls deploy
```

## APIのテスト

curlコマンドを使ってAPIのテストをすることができます。
`<id_token>`には Cognito ユーザーのIDトークンをコピーしてください。
APIのURLはデプロイ後にコンソールに表示されるURLに置き換えます。

```bash
curl --header "Authorization: bearer <id_token>" https://{api}.execute-api.{region}.amazonaws.com/api/privat
```

## おわり

AWS上に作成したリソースは以下のコマンドで削除できます。

```bash
sls remove
```