Serverless framework + API Gateway + Lambda で `--stage` の値を判別してステージング環境に基本認証


Serverless framework を使うと sls deploy --stage production とか sls deploy --stage staging みたいな感じで、本番とステージングを切り替えてデプロイするが、この --stage の値を判別してステージングへのデプロイだけ基本認証をかけるやり方。

serverless.yml

serverless.yml
functions:
  app:
    handler: handler.app
    events: ${self:custom.events.${opt:stage,'staging'}}
  authorizer:
    handler: lib/authorizer.handler


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

custom:
  events:
    production:
      - http:
          path: /
          method: get
      - http:
          path: /{any+}
          method: get
    staging:
      - http:
          path: /
          method: get
          authorizer:
            name: authorizer
            resultTtlInSeconds: 0
            identitySource: method.request.header.Authorization
            type: request
      - http:
          path: /{any+}
          method: get
          authorizer:
            name: authorizer
            resultTtlInSeconds: 0
            identitySource: method.request.header.Authorization
            type: request

  • functions.app.events の値 ${self:custom.events.${opt:stage,'staging'}} は、下にある custom.events.* の値を読み込むための記述。
  • functions.authorizer には認証用のハンドラを指定する。ハンドラの処理は後述。
  • resources.Resources.GatewayResponse 以下の記述は基本認証を行うためのレスポンスヘッダーを返すための設定。
  • custom 以下の記述は URL の設定及び基本認証を行うためのカスタムオーソライザーの設定。

認証用ハンドラ

'use strict';

exports.handler = (event, context, callback) => {
  const authorizationHeader = event.headers.Authorization

  if (!authorizationHeader) return callback('Unauthorized')

  const encodedCreds = authorizationHeader.split(' ')[1]
  const plainCreds = (new Buffer(encodedCreds, 'base64')).toString().split(':')
  const username = plainCreds[0]
  const password = plainCreds[1]

  if (!(username === 'admin' && password === 'secret')) return callback('Unauthorized')

  const authResponse = buildAllowAllPolicy(event, username);
  callback(null, authResponse)
}

const buildAllowAllPolicy = (event, principalId) => {
  const tmp = event.methodArn.split(':')
  const apiGatewayArnTmp = tmp[5].split('/')
  const awsAccountId = tmp[4]
  const awsRegion = tmp[3]
  const restApiId = apiGatewayArnTmp[0]
  const stage = apiGatewayArnTmp[1]
  const apiArn = `arn:aws:execute-api:${awsRegion}:${awsAccountId}:${restApiId}/${stage}/*/*`
  const policy = {
    principalId: principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: 'Allow',
          Resource: [apiArn]
        }
      ]
    }
  }
  return policy
}

ソースコードは以下の記事を参考にしました。

ユーザー名は admin、パスワードは secret で、ソースコード内に決め打ちで入ってますので注意。