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

[前回作成したサーバーレスLaravel](https://qiita.com/umihico/items/64fcf159f68ebd866170)ですが、BASIC認証を付与しようとしたら躓きました。
**API Gatewayを経由すると、`WWW-Authenticate`ヘッダーがx-amazn-remapped-www-authenticateに置き換えらます。その結果BASIC認証を求めるポップを表示できず無条件で`４０１ Unauthorized`エラーだけを表示するWEBになります。**

つまり問題はLambdaではなく、API Gatewayです。ALBでURLを設定すればこの問題はありません。ALB経由のLambdaを使ったサーバーレスLaravelのBASIC認証は通常のLaravelと同じです。[53eda06](https://github.com/umihico/laravel-demo/commit/53eda06)

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

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

オーソライザー用LambdaはメインのLambdaと同じ言語である必要は無いという意味で言語不問です。この記事では新規にプロジェクトを作成し、nodejsのオーソライザーを付与します。上記Laravelはオーソライザーも[素のPHPのLambda関数](https://qiita.com/umihico/items/ff21956caf90e9d831ee)で実装したかったですが、うまくいかずnodejsで実装しています。[cbb1ad6](https://github.com/umihico/laravel-demo/commit/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フォーラム](https://forums.aws.amazon.com/thread.jspa?threadID=233746)を見たところ、既知の問題のようです。実務アプリでは例えば401ではなく/loginにリダイレクトさせ、ブラウザーにBASIC認証用のヘッダーを持たせる責務を１つの静的パスに集約させるなどの対策が必要です。それかAPI Gatewayからロードバランサーに切り替えるなど。

```bash
$ # 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](https://github.com/umihico/authorizer-demo/commit/6b95a36)

```diff: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
```

```bash
$ 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](https://github.com/umihico/authorizer-demo/commit/dcb8407)

```diff: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,
```

```diff: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
```

```bash
$ 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](https://github.com/umihico/authorizer-demo/commit/e167c22)

```diff: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: {
```

```diff: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認証を求めるポップを表示してくれるようになりました。

参考

+ https://medium.com/@Da_vidgf/http-basic-auth-with-api-gateway-and-serverless-5ae14ad0a270
+ https://forum.serverless.com/t/http-basic-auth-with-lambda-api-gateway/4305
+ https://github.com/serverless-projects/serverless-authorizers/
+ https://forums.aws.amazon.com/thread.jspa?threadID=233746
+ https://dev.classmethod.jp/articles/lambda-authorizer/