プライベートなCloudFrontと認証して閲覧許可するAPIをAWS CDKで構築してみる

CloudFrontの署名付きCookie機能を利用して、プライベートなCloudFrontと認証して閲覧許可するAPIをAWS CDKで構築してみます。どんな動作になるかは完成イメージの動画を見てください。
2023.04.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

静的サイトを構築したい!って時は、S3を利用すれば簡単に静的Webサイトを公開できます。 さらに前段にCloudFrontを置けば、高速化、HTTPS化することもできます。

CloudFront + S3は、静的ページをパブリックに公開したい場合の鉄板構成だと思います。

パブリックな静的サイトだけでなく、社内ページや一部のユーザーにだけ限定的に公開したいケースもあると思います。

そんな時は、CloudFrontにプライベートコンテンツの配信を行うための署名付きCookieという機能があります。 詳しくはこちらの弊社ブログを御覧ください。

そこで、今回はCloudFrontの署名付きCookieを発行してブラウザにセットするAPIをAWS CDK(v2)で作ってみようと思います。

完成イメージ

完成イメージを作ったんで、この動画を見てください。こんな感じのものを作ります。

構成概要図

ざっくりこんな感じのものを作ります。

CloudFrontのマルチオリジン機能を利用してパスベースで振り分けます。 /auth/* のパスで振り分けるログインページを提供するS3と、 /auth-api/* のパスで振り分ける認証処理をするAPI Gateway+Lambdaと、 /* のパスで振り分ける署名付きCookieで制限したメインコンテンツを提供するS3です。

ざっくり次のようなフローを取ります。

  1. S3へ保存したログインページへアクセスする
  2. ブラウザでID, Passwordを入力してログインする
  3. ID, PasswordをAPI GatewayへPOSTする
  4. Lambdaで認証処理、およびCloudFront用の署名付きCookieを生成する
  5. レスポンスヘッダーで署名付きCookieを取得し、ブラウザのCookieに保存する
  6. 署名付き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 に対して、useridpassword を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 オブジェクトに詰め直しています。

index.ts

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にしてます。

index.ts

    // 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に割り当てられたデフォルトドメイン名

index.ts

    const {privateKey, keyPairId, cloudFrontDomain} = await getParameters();

90行目〜111行目で期限1分のCloudFront用署名付きCookieを作成しています。

このロジックは次の弊社ブログの内容とほぼ同じです。詳しくはこちらを御覧ください。

113行目あたりから、作成した署名付きCookieをブラウザで保存できるように Set-Cookie ヘッダーに署名付きCookieの値を詰め込んでいます。

複数の値を詰める必要があるので headers ではなく multiValueHeaders を利用しています。 詳しくは次のドキュメントを御覧ください。

index.ts

    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に簡易的な閲覧制限をつけるには、こんな方法もあるというご紹介でした。