概要

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

デプロイには Serverless を使用しました。 コードはこちらでも公開しています。

Lambda Authorizer とは

Lambda 関数を使用して API へのアクセスを制御する API Gateway の機能です。 API Gateway Lambda オーソライザーの使用

Authorizer 関数の実装

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

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 関数の実装

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

デプロイする

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

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

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 を作成し、必要なライブラリをインストールします。

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

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

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

API のテスト

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

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

おわり

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

sls remove