CloudFront FunctionsでBasic認証ができるか試してみた

# はじめに

結論を申し上げるとできませんでした。悲しみが深いです。
「用法用量を守って正しくお使いください」という言葉がぴったりです。
Basic認証をCloudFrontで実装したいならLambda@Edgeを使いましょう。

参考資料:

- [https://michimani.net/post/aws-about-runtime-of-cloudfront-functions/](https://michimani.net/post/aws-about-runtime-of-cloudfront-functions/)
- [https://dev.classmethod.jp/articles/cloudfront-functions-usecases/](https://dev.classmethod.jp/articles/cloudfront-functions-usecases/)
- [https://aws.amazon.com/jp/blogs/news/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/](https://aws.amazon.com/jp/blogs/news/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/)

## 追記(2021/05/24)
コメントをいただき、base64でエンコードできる方法が見つかったため、
Basic認証を実行することができます。

# なぜBasic認証ができないのか

これはCloudFront Functionsの1 msの起動時間制限に引っかかるためだと思います。

CloudFront Functionsの用途として非常に高速でシンプルな HTTP(S) リクエスト/レスポンス操作することを目的としています。そのため、非同期処理等の時間がかかる複雑なものは対象外(Lambda@Edge使えよ)だったのだと思います。

また、使える関数も少なく、パッケージもインポートできないため、非常にシンプルな実装しかできませんでした。

参考資料:

- [https://aws.amazon.com/jp/blogs/news/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/](https://aws.amazon.com/jp/blogs/news/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/)

そのため、参考資料のAWSブログで書かれている4つの用途で使うのが適任だと感じました。

- キャッシュキーの操作と正規化
- URL の書き換えとリダイレクト
- HTTP ヘッダーの操作
- アクセス承認

またサンプルコードの[GitHubリポジトリ](https://github.com/aws-samples/amazon-cloudfront-functions)があるので、こちらを参照すると実装のイメージが湧くと思います。

# Basic認証実装コード

※できないけど、実際に実装したコードを晒そうと思います。このコードではCloudFront FunctionsでBasic認証できません

CloudFront Functionsのランタイムは `JavaScript(ECMAScript 5.1 compliant)` と古く、使えるBuild-in modulesもCryptoとQuery stringしかないっぽいです。

参考資料:

- [https://michimani.net/post/aws-about-runtime-of-cloudfront-functions/](https://michimani.net/post/aws-about-runtime-of-cloudfront-functions/)

そのため、自前でBasic認証を実現するためにBase64にする処理を自前で書く必要があります(btoa関数もないので、文字列をBase64に変換することは無理)。

```jsx
function base64Encode(...parts) {
  return new Promise(resolve => {
    var reader = new FileReader();
    reader.onload = () => {
      var offset = reader.result.indexOf(",") + 1;
      resolve(reader.result.slice(offset));
    };
    reader.readAsDataURL(new Blob(parts));
  });
}
```

Basic認証をさせたい場合は `www-authenticate` ヘッダの値に `Basic` をいれればすることが可能なので、認証が通らない場合はこのようにします。

参考資料:

- [https://www.atmarkit.co.jp/ait/articles/1608/10/news021.html](https://www.atmarkit.co.jp/ait/articles/1608/10/news021.html)

```jsx
var response401 = {
    statusCode: 401,
    statusDescription: 'Unauthorized',
    headers:
    {'www-authenticate': {value:'Basic'}}
};
```

CloudFront Funcitonsのメイン関数はLambdaやLambda@Edgeと似てますが、 `context`や `callback` がなく `event` のみ受け取ります。

```jsx
function handler(event) {
   var request = event.request;
   return request;
}
```

最終的にこのようなコードを書きました。
base64Encodeの部分はpromiseで処理をしているため、
await/asyncを使いたかったのですが、`JavaScript` のバージョン制約で使えなかったです。

base64Encodeの戻り値から処理を行いresponseに代入するようにしてますが、処理時間の制約事項のため `promise {}` しか返して来ずBase64のエンコードはできませんでした。

```jsx
var response401 = {
    statusCode: 401,
    statusDescription: 'Unauthorized',
    headers:
    {'www-authenticate': {value:'Basic'}}
};

function base64Encode(...parts) {
  return new Promise(resolve => {
    var reader = new FileReader();
    reader.onload = () => {
      var offset = reader.result.indexOf(",") + 1;
      resolve(reader.result.slice(offset));
    };
    reader.readAsDataURL(new Blob(parts));
  });
}

function handler(event) {
    // NOTE: This example function is for a viewer request event trigger. 
    // Choose viewer request for event trigger when you associate this function with a distribution. 
    // Get request and request headers
    var request = event.request;
    var headers = request.headers;
    // Configure authentication
    var authUser = 'user';
    var authPass = 'pass';
    var response = base64Encode(authUser + ':' + authPass).then((encoded) => {
        var authString =  'Basic ' + encoded;
        if (typeof headers.authorization == 'undefined' || headers.authorization.value != authString) {
          return response401;
        }
        return request;
    });
    return response;
}
```

## コード修正版 追記(2021/05/24)
`btoa` 関数を使わなくても `toString('base64')` でエンコードすることができました。

```jsx
var response401 = {
    statusCode: 401,
    statusDescription: 'Unauthorized',
    headers:
    {'www-authenticate': {value:'Basic'}}
};

function handler(event) {
    // NOTE: This example function is for a viewer request event trigger. 
    // Choose viewer request for event trigger when you associate this function with a distribution. 
    // Get request and request headers
    var request = event.request;
    var headers = request.headers;
    // Configure authentication
    var authUser = 'user';
    var authPass = 'pass';
    var authString = 'Basic ' + (authUser + ':' + authPass).toString('base64');
    if (typeof headers.authorization == 'undefined' || headers.authorization.value != authString) {
        return response401;
    }
    
    return request;
}
```

# おわりに
~~Basic認証ではなく、Digest認証だとCloudFront Functionsで実装できそうでしたが、そこまで頑張るならLambda@Edgeでいいかという結論に至りました。~~

CloudFront Functionsで気軽にBasic認証もできるので、Lambda@Edge以外の選択肢も視野に入れてみるといいかもしれません。
※コードを読んでお気づきかと思いますが、Basic認証は文字列を単純に結合して、base64でエンコードするだけなので、めちゃくめちゃセキュリティ的に弱いので、あまり使わない方がいいです