API Gateway LambdaオーソライザーでのBasic認証をServerlessで実践!

こんにちは!フリージアの東山です。
今回は、API Gateway Lambdaオーソライザーの機能を使って、API Gatewayで公開しているエンドポイントにBasic認証をかける方法を説明しようと思います。

API Gatewayは、AWS (Amazon Web Service) の提供しているサービスの1つで、「あらゆる規模のRESTおよびWebSocket APIを作成、公開、保守、モニタリング、および保護する」ことが出来ます。
詳細はAmazon API Gateway とは?をご覧ください。

API Gatewayの機能の1つに、Lambda関数を使用してAPIへのアクセスを制御するLambdaオーソライザーがあります。
Lambdaオーソライザーでのアクセス制御フローは以下になります。

  1. クライアントがAPI Gatewayのメソッドを呼び出す
  2. API Gatewayは、メソッドに対してLambdaオーソライザーが設定されているかどうかを確認し、設定されている場合は、発信者IDを入力としてLambda関数に渡す
  3. Lambda関数は、発信者IDをBearerトークンまたはリクエストパラメータとして受け取り、IAMポリシーを出力として返却する
  4. API Gatewayは、受け取ったIAMポリシーを評価し、その結果、アクセスが許可されている場合はメソッドを実行する

今回は、この機構を利用して、Basic認証を実装しようと思います。

テスト用エンドポイントの作成と公開

まず始めに、API Gatewayにテスト用のエンドポイントを作成・公開しましょう。

テスト用エンドポイントの作成

テスト用のアプリケーションは、Node.jsのExpressフレームワークで作成します。
本記事の趣旨とはズレますので、説明は割愛します。Hello World の例 と同じ内容となっていますので、気になる方はご覧ください。

# プロジェクトディレクトリの作成
$ mkdir lambda-authorizer-serverless
$ cd lambda-authorizer-serverless

$ yarn init
$ yarn add express

$ vi app.js

app.jsは、以下の内容で作成します。

// app.js

const express = require('express')
const app = express()

app.get('/', (req, res) => res.send('Hello World!'))

app.listen(3000, () => console.log('Example app listening on port 3000!'))

このアプリケーションは、サーバを始動して、3000番ポートで接続をlistenします。
以下のコマンドでアプリケーションを実行後、「http://localhost:3000/」にアクセスし、「Hello World!」が表示されることを確認して下さい。

$ node app.js

screenshot express 1004

テスト用エンドポイントの公開

先程作ったExpressアプリケーションを、早速公開しましょう。

API Gateway × Lambdaでの動作環境を、serverlessフレームワークで構築します。
まずは、必要なパッケージをインストールして下さい。

$ yarn add -D serverless
$ yarn add aws-serverless-express

次に、serverlessの設定を行います。
プロジェクトディレクトリ配下に「serverless.yml」を以下の内容で作成して下さい。

# serverless.yml

service: lambda-authorizer-serverless

provider:
  name: aws
  runtime: nodejs10.x
  region: ap-northeast-1
  stg: ${opt:stage}

functions:
  app:
    handler: app.handler
    events:
      - http:
          path: '/'
          method: any
      - http:
          path: '{proxy+}'
          method: any

今回は、serverlessの標準設定での構成となっています。
serviceセクションはサービスの定義、providerセクションは動作環境の指定、functionsセクションはLambda関数 (app) のhandlerメソッドの指定およびAPI Gatewayの設定をそれぞれ行っています。

次に、Lambda関数を呼び出した際に実行される、handlerメソッドを作成します。
app.jsを次のように修正して下さい。

// app.js

const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello World!'))

const awsServerlessExpress = require('aws-serverless-express')
const server = awsServerlessExpress.createServer(app)

exports.handler = (event, context) => { awsServerlessExpress.proxy(server, event, context) }

ExpressアプリケーションをAPI Gateway × Lambdaの環境で動かすために、Amazon公式パッケージであるaws-serverless-expressを利用しています。
Lambda関数のhandlerメソッドが実行された際に、awsServerlessExpressがExpressアプリケーションにリクエストをプロキシします。

以上で、serverlessの設定は完了です。

最後に、以下のコマンドを実行し、AWSへアプリケーションをデプロイして下さい。
(※ AWSへの登録や、AWS アクセスキーの取得がまだの方は、AWS アカウントとアクセスキーを実施して下さい)

# {YOUR-XXX}は適宜置き換えて下さい
$ AWS_ACCESS_KEY_ID={YOUR-KEY} AWS_SECRET_ACCESS_KEY={YOUR-SECRET-KEY} yarn sls deploy --stage prod
yarn run v1.17.3
$ /path/to/lambda-authorizer-serverless/node_modules/.bin/sls deploy --stage prod
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service lambda-authorizer-serverless.zip file to S3 (802.01 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.......................
Serverless: Stack update finished...
Service Information
service: lambda-authorizer-serverless
stage: prod
region: ap-northeast-1
stack: lambda-authorizer-serverless-prod
resources: 11
api keys:
  None
endpoints:
  ANY - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
  ANY - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/{proxy+}
functions:
  app: lambda-authorizer-serverless-prod-app
layers:
  None
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
✨  Done in 43.60s.

ログで出力される、

https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/

にアクセスし、「Hello World!」が表示されれば、テスト用エンドポイントの公開は完了です。

LambdaオーソライザーでのBasic認証の実装

長くなりましたが、いよいよ本題のLambdaオーソライザーでのBasic認証の実装です。

実装手順は次のようになります。

  1. API Gatewayのメソッドに対するLambdaオーソライザーの設定
  2. Lambdaオーソライザーで用いるLambda関数の実装
  3. AWSへのデプロイおよびBasic認証の検証

では早速、実装していきましょう!

API Gatewayのメソッドに対するLambdaオーソライザーの設定

AWSマネジメントコンソールにアクセスして〜、ではなく、serverlessで設定します。
serverless.ymlに対し、次の設定を追記して下さい。

# serverless.yml

service: # 省略

provider: # 省略

custom:
  definitions:
    authorizer:
      # Lambda関数名
      name: authorizer
      # キャッシュ時間
      resultTtlInSeconds: 0
      # Lambda関数に渡すヘッダー名
      identitySource: method.request.header.Authorization
      # Lambdaイベントペイロード
      type: request

functions:
  app:
    handler: app.handler
    events:
      - http:
          path: '/'
          method: any
          # Lambdaオーソライザーの設定
          authorizer: ${self:custom.definitions.authorizer}
      - http:
          path: '{proxy+}'
          method: any
          # Lambdaオーソライザーの設定
          authorizer: ${self:custom.definitions.authorizer}
  # Lambda関数 (authorizer) のhandlerメソッドの指定
  authorizer:
    handler: authorizer.handler

resources:
  Resources:
    GatewayResponse:
      Type: 'AWS::ApiGateway::GatewayResponse'
      Properties:
        ResponseParameters:
          gatewayresponse.header.WWW-Authenticate: "'Basic'"
        ResponseType: UNAUTHORIZED
        RestApiId:
          Ref: 'ApiGatewayRestApi'
        StatusCode: '401'

さっきまで無かったセクションとして、customとresourcesを追加しました。

customセクションは変数定義で用いられるセクションです。 例えば、Serverless Pluginsを導入した場合、各プラグインの設定変数などはcustomセクションで定義します。
今回は、認証に係る設定を独自変数としてcustomセクションに定義しています。

resourcesセクションは、AWSリソースの設定を行なうセクションです。
今回は、Lambdaオーソライザーが401エラーを返却した場合、「WWW-Authenticate: 'Basic'」ヘッダーをクライアントに返すようAPI Gatewayのゲートウェイレスポンスを設定しています。

また、functionsセクションには、API Gatewayのメソッドに対するLambdaオーソライザーの設定Lambda関数 (authorizer) のhandlerメソッドの指定を追記しています。

以上で、Lambdaオーソライザーの設定は完了です。

Lambdaオーソライザーで用いるLambda関数の実装

Lambda関数 (authorizer) を呼び出した際に実行される、handlerメソッドを作成します。
プロジェクトルート配下に、authorizer.jsを次の内容で作成して下さい。

// authorizer.js

// Authorizationヘッダーの照合
module.exports.handler = (event, context, callback) => {
  const authorizationHeader = event.headers.Authorization
  if (!authorizationHeader) {
    return callback('Unauthorized')
  }

  const encodedCredentials = authorizationHeader.split(' ')[1]
  const [ username, password ] = (Buffer.from(encodedCredentials, 'base64')).toString().split(':')
  if (!(username === 'admin' && password === 'password')) {
    return callback('Unauthorized')
  }

  const authResponse = buildPolicy(event, username)
  callback(null, authResponse)
}

// IAMポリシーの作成
function buildPolicy (event, principalId) {
  const [ identifier, service, action, region, accountId, apiGatewayArn ] = event.methodArn.split(':')
  const [ apiId, stage, ..._rest ] = apiGatewayArn.split('/')

  return {
    principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          // 実行アクション
          Action: 'execute-api:Invoke',
          // 許可
          Effect: 'Allow',
          // API Gatewayエンドポイント
          Resource: [`${identifier}:${service}:${action}:${region}:${accountId}:${apiId}/${stage}/*/*`]
        }
      ]
    }
  }
}

ここでは大きく分けて2つのことを行なっています。

  • Authorizationヘッダーからユーザ名、パスワードを取り出し照合する
  • IAMポリシーを作成する

Authorizationヘッダーの照合に成功すれば作成したIAMポリシーを、失敗すればUnAuthorizedエラーを返却します。
IAMポリシーが返却された場合、API Gatewayはポリシーを評価し、メソッドの実行を実行します。

AWSへのデプロイおよびBasic認証の検証

では、AWSへアプリケーションをデプロイし、Basic認証が有効になっていることを確認しましょう!
先程同様、serverless deployコマンドを実行して下さい。

# {YOUR-XXX}は適宜置き換えて下さい
$ AWS_ACCESS_KEY_ID={YOUR-KEY} AWS_SECRET_ACCESS_KEY={YOUR-SECRET-KEY} yarn sls deploy --stage prod
...

API Gatewayエンドポイントにアクセスし、Basic認証のポップアップが表示されれば成功です。
なお、ユーザ名、パスワードはauthorizer.jsで指定したものになります。

以上で、検証終了です。
ここまでのソースコードはfreegian/lambda-authorizer-serverlessにプッシュしていますので、参照下さい。

最後に

今回は、API Gateway Lambdaオーソライザーの機能を使って、API Gatewayで公開しているエンドポイントにBasic認証をかけてみました。
また、AWSの環境構築をserverlessフレームワークで実践しました。

API Gateway Lambdaオーソライザーを使えば、OAuthやBasic認証のようなBearerトークンを利用する認可方式だけでなく、リクエストパラメーターを使用するカスタム認証方式も簡単に実装できます。
IP制限を用いない社内テスト環境の構築や、外部クライアントに対する仮想環境の提供など、認証認可が発生する様々なシーンで活用できる機能だと思いますので、その際にはぜひ検討してみて下さい。

本記事を見て、ご意見やご指摘等ございましたら、「[email protected]」まで是非ご連絡ください。