LambdaのオーソライザーでBASIC認証を追加する【言語不問】


前回作成したサーバーレスLaravelですが、BASIC認証を付与しようとしたら躓きました。
API Gatewayを経由すると、WWW-Authenticateヘッダーがx-amazn-remapped-www-authenticateに置き換えらます。その結果BASIC認証を求めるポップを表示できず無条件で401 Unauthorizedエラーだけを表示するWEBになります。

つまり問題はLambdaではなく、API Gatewayです。ALBでURLを設定すればこの問題はありません。ALB経由のLambdaを使ったサーバーレスLaravelのBASIC認証は通常のLaravelと同じです。53eda06

API Gateway経由でもBASIC認証を導入するには以下のように変更します。

  • 別途新規のLambda関数を作成し、API Gateway上にオーソライザーとして登録する。
  • オーソライザーのレスポンスにWWW-Authenticateを付与する

オーソライザー用LambdaはメインのLambdaと同じ言語である必要は無いという意味で言語不問です。この記事では新規にプロジェクトを作成し、nodejsのオーソライザーを付与します。上記Laravelはオーソライザーも素のPHPのLambda関数で実装したかったですが、うまくいかずnodejsで実装しています。cbb1ad6

しかしながら、この方法だと- http: 'ANY /{proxy+}'などダイナミックなルーティングをした場合に

An error occurred: ApiGatewayResourceUuidVarhtml - Resource's path part only allow a-zA-Z0-9._- and curly braces at the beginning and the end. 

というエラーが発生します。つまり静的なパスにしか設定できません。AWSフォーラムを見たところ、既知の問題のようです。実務アプリでは例えば401ではなく/loginにリダイレクトさせ、ブラウザーにBASIC認証用のヘッダーを持たせる責務を1つの静的パスに集約させるなどの対策が必要です。それかAPI Gatewayからロードバランサーに切り替えるなど。

$ # https://github.com/umihico/authorizer-demo/commit/c0d1263
$ mkdir authorizer-demo
$ cd authorizer-demo
$ serverless create --template aws-nodejs --name authorizer-demo #プロジェクト作成

API Gatewayを設定して、まずオーソライザー無し動作確認します。6b95a36

serverless.yml
functions:
  hello:
    handler: handler.hello
+     events:
+       - http:
+           path: hello
+           method: get
#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
$ serverless deploy
$ curl https://7xcxsu7mp5.execute-api.us-east-1.amazonaws.com/dev/hello
{message: "Go Serverless v1.0! Your function executed successfully!",input: {省略}}

BASIC認証の前にトークンを使った認証を作ってテストします。dcb8407

handler.js
'use strict';


+ module.exports.auth = (event, context, callback) => {
+     var token = event.headers["Authorization"];
+     if (token == 'password') {
+         callback(null, {
+             principalId: 'user',
+             policyDocument: {
+                 Version: '2012-10-17',
+                 Statement: [{
+                     Action: 'execute-api:Invoke',
+                     Effect: 'Allow',
+                     Resource: "*",
+                 }]
+             }
+         });
+     } else {
+         callback('Unauthorized');
+     }
+ };

module.exports.hello = async event => {
  return {
    statusCode: 200,
serverless.yml
 - http:
    path: hello
    method: get
+     authorizer:
+       name: auth
+       resultTtlInSeconds: 0
+       type: request
+ auth:
+ handler: handler.auth
#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
$ curl https://7xcxsu7mp5.execute-api.us-east-1.amazonaws.com/dev/hello
{message: "Unauthorized"}
$ curl --header "Authorization: password" https://7xcxsu7mp5.execute-api.us-east-1.amazonaws.com/dev/hello
{message: "Go Serverless v1.0! Your function executed successfully!",input: {省略}}
$ curl --header "Authorization: dummy" https://7xcxsu7mp5.execute-api.us-east-1.amazonaws.com/dev/hello
{message: "Unauthorized"}

正しいパスワードがセットされているときのみ動作してくれるようになりました。
WEBアクセスしたらBASIC認証のポップを出してくれるようにリソースを設定します。e167c22

handler.js
'use strict';

module.exports.auth = (event, context, callback) => {
-     var token = event.headers["Authorization"];
-     if (token == 'password') {
+     let Authorization = event.headers.Authorization;
+     if (!Authorization) return callback('Unauthorized');
+     let [username, password] = (new Buffer(Authorization.split(' ')[1], 'base64')).toString().split(':');
+     if (username === 'admin' && password === 'secret4') {
        callback(null, {
            principalId: 'user',
            policyDocument: {
serverless.yml
#    environment:
#      variable2: value2

+ resources:
+   Resources:
+     GatewayResponse:
+       Type: 'AWS::ApiGateway::GatewayResponse'
+       Properties:
+         ResponseParameters:
+           gatewayresponse.header.WWW-Authenticate: "'Basic'"
+         ResponseType: UNAUTHORIZED
+         RestApiId:
+           Ref: 'ApiGatewayRestApi'
+         StatusCode: '401'
# you can add CloudFormation resource templates here
#resources:
#  Resources:

デプロイしてURLにアクセスするとBASIC認証を求めるポップを表示してくれるようになりました。

参考