はじめに
結論を申し上げるとできませんでした。悲しみが深いです。
「用法用量を守って正しくお使いください」という言葉がぴったりです。
Basic認証をCloudFrontで実装したいならLambda@Edgeを使いましょう。
参考資料:
- https://michimani.net/post/aws-about-runtime-of-cloudfront-functions/
- 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/
追記(2021/05/24)
コメントをいただき、base64でエンコードできる方法が見つかったため、
Basic認証を実行することができます。
なぜBasic認証ができないのか
これはCloudFront Functionsの1 msの起動時間制限に引っかかるためだと思います。
CloudFront Functionsの用途として非常に高速でシンプルな HTTP(S) リクエスト/レスポンス操作することを目的としています。そのため、非同期処理等の時間がかかる複雑なものは対象外(Lambda@Edge使えよ)だったのだと思います。
また、使える関数も少なく、パッケージもインポートできないため、非常にシンプルな実装しかできませんでした。
参考資料:
そのため、参考資料のAWSブログで書かれている4つの用途で使うのが適任だと感じました。
- キャッシュキーの操作と正規化
- URL の書き換えとリダイレクト
- HTTP ヘッダーの操作
- アクセス承認
またサンプルコードのGitHubリポジトリがあるので、こちらを参照すると実装のイメージが湧くと思います。
Basic認証実装コード
※できないけど、実際に実装したコードを晒そうと思います。このコードではCloudFront FunctionsでBasic認証できません
CloudFront Functionsのランタイムは JavaScript(ECMAScript 5.1 compliant)
と古く、使えるBuild-in modulesもCryptoとQuery stringしかないっぽいです。
参考資料:
そのため、自前でBasic認証を実現するためにBase64にする処理を自前で書く必要があります(btoa関数もないので、文字列をBase64に変換することは無理)。
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
をいれればすることが可能なので、認証が通らない場合はこのようにします。
参考資料:
var response401 = {
statusCode: 401,
statusDescription: 'Unauthorized',
headers:
{'www-authenticate': {value:'Basic'}}
};
CloudFront Funcitonsのメイン関数はLambdaやLambda@Edgeと似てますが、 context
や callback
がなく event
のみ受け取ります。
function handler(event) {
var request = event.request;
return request;
}
最終的にこのようなコードを書きました。
base64Encodeの部分はpromiseで処理をしているため、
await/asyncを使いたかったのですが、JavaScript
のバージョン制約で使えなかったです。
base64Encodeの戻り値から処理を行いresponseに代入するようにしてますが、処理時間の制約事項のため promise {}
しか返して来ずBase64のエンコードはできませんでした。
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')
でエンコードすることができました。
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でエンコードするだけなので、めちゃくめちゃセキュリティ的に弱いので、あまり使わない方がいいです