プライベートなCloudFrontと認証して閲覧許可するAPIをAWS CDKで構築してみる
静的サイトを構築したい!って時は、S3を利用すれば簡単に静的Webサイトを公開できます。 さらに前段にCloudFrontを置けば、高速化、HTTPS化することもできます。
CloudFront + S3は、静的ページをパブリックに公開したい場合の鉄板構成だと思います。
パブリックな静的サイトだけでなく、社内ページや一部のユーザーにだけ限定的に公開したいケースもあると思います。
そんな時は、CloudFrontにプライベートコンテンツの配信を行うための署名付きCookieという機能があります。 詳しくはこちらの弊社ブログを御覧ください。
そこで、今回はCloudFrontの署名付きCookieを発行してブラウザにセットするAPIをAWS CDK(v2)で作ってみようと思います。
完成イメージ
完成イメージを作ったんで、この動画を見てください。こんな感じのものを作ります。
構成概要図
ざっくりこんな感じのものを作ります。
CloudFrontのマルチオリジン機能を利用してパスベースで振り分けます。
/auth/*
のパスで振り分けるログインページを提供するS3と、
/auth-api/*
のパスで振り分ける認証処理をするAPI Gateway+Lambdaと、
/*
のパスで振り分ける署名付きCookieで制限したメインコンテンツを提供するS3です。
ざっくり次のようなフローを取ります。
- S3へ保存したログインページへアクセスする
- ブラウザでID, Passwordを入力してログインする
- ID, PasswordをAPI GatewayへPOSTする
- Lambdaで認証処理、およびCloudFront用の署名付きCookieを生成する
- レスポンスヘッダーで署名付きCookieを取得し、ブラウザのCookieに保存する
- 署名付きCookieを利用して、CloudFrontのメインコンテンツへアクセスする
サンプルプログラム置き場
今回作成したcdkのプログラムはGitHubで公開しています。実際に動かしてみたい方はこちらを御覧ください。
前提
CloudFrontの署名付きCookieの機能を利用するために、公開鍵と秘密鍵のキーペアが必要です。 秘密鍵を利用して署名付きCookieを作成し、CloudFrontで公開鍵を使用して署名を検証します。
次のドキュメントを参考に、ローカル端末でキーペアを作成します。
$ openssl genrsa -out private_key.pem 2048 $ openssl rsa -pubout -in private_key.pem -out public_key.pem
作成した公開鍵はAWSのParameter Storeに、get-signed-cookies-sample-public-key
という名前で保存してください。
秘密鍵はSecrets Managerに、 get-signed-cookies-sample-private-key
という名前で保存してください。
それぞれ、cdkのプログラム内で利用します。
AWS CDK(v2)を使ったAWS環境の構築
まずは git clone
して環境一式をGitHubから取ってきます。
$ git clone https://github.com/rednes/cdk-cloudfront-signed-cookies-sample.git $ cd cdk-cloudfront-signed-cookies-sample
次に npm install
コマンドを実行して AWS CDK実行に必要なパッケージをインストールします。
$ npm install
AWS環境に合わせて環境変数を設定します。
$ export AWS_DEFAULT_PROFILE=<<YOUR AWS PROFILE>> $ export CDK_DEFAULT_ACCOUNT=<<YOUR AWS ACCOUNT>> $ export CDK_DEFAULT_REGION=<<YOUR AWS REGION>>
次のデプロイコマンドを実行すると、AWS環境が構築できます。
$ npm run cdk:deploy
デプロイが完了すると、CloudFrontのURLが出力されるのでブラウザからアクセスしてみます。
プライベートなCloudFrontを構築しているので、そのままではアクセスできません。
ログインページからログインして(ちなみにIDとパスワードは空じゃなければ何でも良いです)。
署名付きCookieを取得、セットした上で再度トップページへアクセスすると。
署名付きCookieを利用してプライベートなCloudFrontへアクセスできるようになります。
AWS環境のおおまかな解説
環境は構築できました。ここからはおおまかなAWS環境の解説をします。
構成図を再掲します。
CloudFrontのマルチオリジン機能を使って、次の3つのオリジンを設定しています。
優先順位 | パスパターン | オリジン | 用途 |
---|---|---|---|
0 | auth/* | S3 | ログインページ用(auth/login.html) |
1 | auth-api/* | API Gateway | 署名付きCookie作成API(auth-api/get-signed-cookies) |
2 | デフォルト(*) | S3 | メインコンテンツ(プライベート) |
メインコンテツのあるデフォルトルートだけ、「ビューワーのアクセスを制限する」を設定して、プライベートなコンテンツにしています。
ログインページ用S3の auth/login.html
に、ログインページを保存しています。
このログインページでやっていることは、フォームを使いAPI Gatewayで構築したAPIである auth-api/get-signed-cookies
に対して、userid
と password
をPOSTしているだけです。
なので、ログインページでは curl
で言えば次のコマンド相当のことをしています。
$ CLOUDFRONT_URL=<<YOUR CLOUDFRONT DISTRIBUTION DOMAIN NAME>> $ curl -i -X POST \ -d 'userid=test&password=testpass' \ ${CLOUDFRONT_URL}/auth-api/get-signed-cookies
そして、このAPIの実行結果として署名付きCookieをセットし、メインコンテンツを閲覧可能にするといった流れです。 署名付きCookieを作成するプログラムについては次章で解説します。
プログラムの解説
このシステムの肝であるCloudFront用の署名付きCookieを作成するLambdaのプログラムの中身について解説します。
プログラムの全文はGitHubで公開していますので、こちらを御覧ください。
66行目からLambdaのハンドラー関数を定義していて、ここから処理が始まります。
68行目で、 event.body
に入っているログインページからPOSTされてきたデータ(userid, password)を使いやすい data
オブジェクトに詰め直しています。
export const handler = async (event: APIGatewayEvent, context: Context) => { // eventからパラメータを取得 const params = new URLSearchParams(event.body); // パラメータを配列からオブジェクトに変換 const data = {}; for (const [key, value] of params) { data[key] = value; }
77行目で、認証処理っぽいことをしていますがサンプルなんで適当です。パスワードが空白の時だけ 401 Unauthorized を返して、それ以外はOKにしてます。
// TODO: 認証ロジックを作り込む if (data['password'] == '') { return { statusCode: 401, headers: { 'Content-Type': 'text/html; charset=UTF-8', }, body: '401 Unauthorized', }; }
88行目で、パラメーターストアやシークレットマネージャーから次の情報を取得しています。
変数名 | 概要 |
---|---|
privateKey | CloudFrontの署名付きCookie作成用の秘密鍵 |
keyPairId | CloudFrontに登録した公開鍵のキーID |
cloudFrontDomain | 構築したCloudFrontに割り当てられたデフォルトドメイン名 |
const {privateKey, keyPairId, cloudFrontDomain} = await getParameters();
90行目〜111行目で期限1分のCloudFront用署名付きCookieを作成しています。
このロジックは次の弊社ブログの内容とほぼ同じです。詳しくはこちらを御覧ください。
113行目あたりから、作成した署名付きCookieをブラウザで保存できるように Set-Cookie
ヘッダーに署名付きCookieの値を詰め込んでいます。
複数の値を詰める必要があるので headers
ではなく multiValueHeaders
を利用しています。
詳しくは次のドキュメントを御覧ください。
const body: string = `Set following cookies:</br> * CloudFront-Key-Pair-Id=${cookies['CloudFront-Key-Pair-Id']}</br> * CloudFront-Signature=${cookies['CloudFront-Signature']}</br> * CloudFront-Policy=${cookies['CloudFront-Policy']} ` const result: APIGatewayProxyResult = { statusCode: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8', }, multiValueHeaders: { 'Set-Cookie': [ `CloudFront-Key-Pair-Id=${cookies['CloudFront-Key-Pair-Id']};expires=${dateLessThan};path=/`, `CloudFront-Signature=${cookies['CloudFront-Signature']};expires=${dateLessThan};path=/`, `CloudFront-Policy=${cookies['CloudFront-Policy']};expires=${dateLessThan};path=/`, ], }, body: body, }; return result;
これでAPI実行後に署名付きCookieがブラウザに保存でき、署名付きCookieを利用したプライベートなCloudFrontへのアクセスが実現できます。
終わりに
プライベートな静的サイトをCloudFrontで作ってみました。
署名付きCookieを利用しているので、作ろうと思えばjavascriptでCookieを削除する動作でログアウト機能も作れます。
サンプルなので認証ロジックは適当ですが、実装次第でCognitoを利用したり他の認証サービスを利用したりすることもできます。
CloudFrontに簡易的な閲覧制限をつけるには、こんな方法もあるというご紹介でした。