Amazon Web Services ブログ

Amazon CloudFront と AWS Lambda@Edge による署名付き Cookie ベースの認証 : パート 1 – 認証

この 2 部構成のブログシリーズでは、メールアドレスとドメイン名を使用してユーザー認証を行う方法を学習します。この方法では、静的な Web サイトへの資格情報を使用しないユーザーアクセスを制限します。

この最初のブログでは、認証メカニズムの実装方法を学びます。第 2 部のブログ記事では、認可メカニズムを実装してソリューションを完成させる方法を学習します。

このソリューションでは、Amazon Simple Storage Service (S3) にコンテンツを保存し、Amazon CloudFront 経由でコンテンツを配信します。AWS Lambda@Edge を介してエンドユーザーへの認証を処理します。AWS Lambda@Edge および Amazon Cognito を使用したログインベースの認証については、このブログを参照してください。

この代替認証ソリューションには、次の 4 つの利点があります。

  • 制限されたコンテンツにアクセスするユーザーに、資格情報を使用しない認証を提供する
  • ユーザー情報に関するデータベース管理は不要
  • ユーザーはサインアップする必要がない
  • このソリューションはサーバレスであり、サーバまたはクラスタのプロビジョニング、パッチ適用、オペレーティングシステムのメンテナンス、キャパシティプロビジョニングなどのインフラストラクチャ管理タスクが不要

このソリューションは General Data Protection Regulation (GDPR) に準拠しています。GDPR への準拠はユーザーデータに関連付けされており、このソリューションではユーザーのメールで処理されますが、この情報は保存されません。

AWS は、お客様のアクティビティに適用される GDPR 要件への準拠を支援するサービスとリソースをお客様に提供することをお約束します。それでも、責任共有モデルにおいてAWS で実行されるアプリケーションが GDPR に準拠しているかどうかは、お客様の責任となります。

ソリューションの概要

下の図 1 は、次に示す認証と認可のワークフローを示しています。

  1. ユーザがログインページに移動すると、ユーザの会社のメールを要求する簡単なフォームが表示される
  2. ユーザーは会社のメールアドレスを入力し、「ログイン」をクリックする
  3. フォームを送信すると API コールが呼び出される。API コールは AWS Lambda@Edge 関数に委任され、ユーザーが送信した E メールアドレスドメインをチェックする
  4. 許可されたドメインと一致した場合、この関数は署名付き URL を生成し、Amazon Simple Email Service を使用してユーザーの E メールに送信する
  5. リンクをクリックすると、ユーザは署名付き URL を使って制限付きコンテンツにルーティングされる
  6. 有効な署名付き URL を指定すると、Amazon CloudFront は署名付き Cookie を返す AWS Lambda@Edge 関数を呼び出す。ブラウザは、制限付きコンテンツにユーザーをリダイレクトする
  7. 署名付き Cookie は後続のリクエストで使用され、Amazon S3 に保存されているファイルへのアクセス権を付与する。これにより、入口となるコンテンツを通じてシームレスなブラウジングエクスペリエンスが可能になる

図 1 : 認証と認可のワークフロー

ウォークスルー

Amazon CloudFront と AWS Lambda@Edge を使用して資格情報を使用せずにコンテンツを保護するには、3 つのステップがあります。

  1. Amazon CloudFront でコンテンツを制限する (コンテンツへのアクセス)
  2. ドメインのチェックと署名付き URL の生成のための AWS Lambda@Edge 関数を作成する (認証)
  3. 署名付き Cookie を生成するために、署名付き URL にアクセスして呼び出されるAWS Lambda@Edge 関数を作成する(認可)

コードスニペットは、ブログ記事を通じて提供されています。

このブログ記事では、ステップ 1 と 2 の実装方法について説明します。

前提条件

このウォークスルーを開始するには、最初に次の手順を実行しておく必要があります。

このブログの残りの部分では、署名付き URL と署名付き Cookie を使用した認証メカニズムに焦点を当てます。

コンテンツへのアクセス

Amazon CloudFront はこのソリューションの主要コンポーネントとなり、以下のように適切に設定する必要があります。

  1. ログインページには制限なくアクセスできます (/login.html)
  2. セキュリティ保護されたコンテンツには、有効な署名付き URL または署名付き Cookie を使用してアクセスされます(/restricted-content.html)
  3. パブリックなアセットには、ログインページとセキュリティ保護されたコンテンツ からアクセスされます(/assets/*)

上記の要件を満たすために、認証されていないユーザーには 3 つの動作、認証されたユーザーには 2 つのビヘイビアを設定します。認証されていないユーザーに対する Amazon CloudFront のビヘイビアは次の 3 つです。

  • login.html
  • /assets/*
  • /login

認証されたユーザーに対する Amazon Cloudfront のビヘイビアは、次の 2 つです。

  • /auth
  • Default(*)

認証されていないユーザーに対して Amazon CloudFront の 3 つのビヘイビアを設定する方法を見てみましょう。

  1. Amazon CloudFront コンソールにログインする
  2. 事前に作成したディストリビューションを選択する
  3. ディストリビューションの設定ページで、[ Behaviors ] を選択する
  4. [ Create behavior ] を選択する
  5. [ Create behavior ][ Settings ][ Path pattern ] に、login.html と記載する
  6. [ Restrict viewer access ](署名付き URL または署名付き Cookie を使用) にて、No を選択して保存する

図 2 : ビヘイビアの作成

CSS や JavaScript ファイルなどのパブリックアクセス可能なアセットは、assets フォルダーに保存されます。login.html と同じ方法でアセットの動作を設定します。

  1. [ Create behavior ] を選択する
  2. [ Create behavior ][ Settings ][ Path pattern ] に、/assets/* を記載する
  3. [ Restrict viewer access ](署名付き URL または署名付き Cookie を使用) にて、前の図 2 に示すように [ No ] を選択し保存する

認証されていないユーザーに対する 3 つのビヘイビアで、ログインパスを設定する必要があります。/login パスは、署名付き URL を作成してメールを送信する AWS Lambda@Edge 関数を呼び出します。

  1. [ Create behavior ] を選択する
  2. [ Create behavior ][ Settings ][ Path pattern ] に、 /login を記載する
  3. [ Restrict viewer access ](署名付き URL または署名付き Cookie を使用) にて、[ No ] を選択し保存する

ここで、認証されたユーザーだけがアクセスできるように制限されたコンテンツを構成します。

  1. [ Create behavior ] を選択する
  2. [ Create behavior ][ Settings ][ Path pattern ] に、 /auth を記載する。このパスにより、署名付き URL を利用して AWS Lambda@Edge 関数によって生成された署名付き Cookie にアクセスする
  3. [ Restrict viewer access ](署名付き URL または署名付き Cookie を使用)にて、[ Yes ] を選択する
  4. [ Trusted key groups ]を選択し、定義済みの信頼済みキーグループの 1 つを選択する。次のドキュメントでは、リクエストに署名するためのキーペアを作成する方法と、Amazon CloudFront ディストリビューションに信頼されたキーグループを追加する方法について説明している
  5. 保存をする

Amazon CloudFront は、すべてのリソースへのアクセスを暗黙的に許可するデフォルトの動作を作成します。残りのすべてのコンテンツを制限するには、次の手順に従います。

  1. “Default” の Behavior を選択し、[ Edit ] をする
  2. [ Restrict viewer access ](署名付き URL または署名付き Cookie を使用)にて、次の図 3 に示すように [ Yes ] を選択する
  3. [ Trusted key groups ] を選択し、定義済みの信頼済みキーグループの 1 つを選択する
  4. 選択された信頼済みキーグループを追加して、変更を保存する

図 3 : Restrict Viewer Access の Trusted Key Groups

以下の図 4 に、制限付きアクセスのビヘイビアを示します。Default と /auth のビヘイビアは、ビューアアクセスが制限されていることを示します。[ Trusted Key Groups ] 列に PublicKeyGroup があることは、Default および /auth のパスパターンがアクセス制限されていることを示します。優先順位は重要です。Amazon CloudFront は優先順位リストの最初の一致を取るため、Default Behavior は最後の優先順位にする必要があります。

図 4 : Amazon CloudFront ディストリビューションにおける制限付きアクセス

設定が完了したら、login.html ページにアクセスしてみます。Amazon CloudFront ドメイン名と login.html へのパス (例:https://dxxxxxxxxxxxxx.cloudfront.net/login.html) を使用すると、以下の図 5 に示すように、制限なしでログインページにアクセスできます。ページは、すべての CSS スタイルを含む assets フォルダーがアクセス可能な状態でレンダリングされます。

図 5 : ログインフォーム

その他のコンテンツは制限されており、https://dxxxxxxxxxxxxx.cloudfront.net/restricted-content.html のようにブラウザからアクセスすることはできません。次の図 6 に示すメッセージが表示されます。

図 6 : 制限付きコンテンツへアクセス可能なキーペアが存在しないエラー

これにより、このコンテンツにアクセスできないことが通知されます。これは Amazon CloudFront のデフォルトのエラーレスポンスです。リクエストには、署名ポリシーのメタデータも署名トークンも含まれていません。特定の HTTP ステータスコードに応じてカスタムエラーページを設定する場合は、このドキュメントを参照してください。login.html ページをカスタムエラーページとして設定する必要があります。リクエストが HTTP ステータスコード 403 (Forbidden) を返すたびに、login.html ページがレンダリングされます。

認証メカニズム

本格的なアイデンティティ管理は不要です。それでも、ユーザーを認証するための設定が必要です。

Amazon CloudFront が認証を実行します。Amazon CloudFront を使用して、署名付き URL や署名付き Cookie など、必要なログイン情報をユーザーが提供しているかどうかを確認します。Amazon CloudFront は、AWS Lambda@Edge または Amazon CloudFront Functions を使用して、エッジで関数を呼び出します。このブログ投稿では、AWS Lambda@Edge 関数を使用して関数をより強力に作成しています。

AWS Lambda@Edge 関数を使用して Amazon CloudFront のリクエストとレスポンスを変更するには、次の時点を使用します。

  1. CloudFront がビューワーからリクエストを受け取った後 (ビューワーリクエスト)
  2. CloudFront がリクエストをオリジンに転送する前 (オリジンリクエスト)
  3. CloudFront がオリジンからレスポンスを受信した後 (オリジンレスポンス)
  4. CloudFront がレスポンスをビューワーに転送する前 (ビューワーレスポンス)

コンテンツを制限する 1 つの方法は、署名付き URL を共有することです。署名付き URL には、有効期限の日時やクライアント IP アドレスなど、検証用の追加情報が含まれます。メールアドレスを入力するフォームをユーザに提供します。メールアドレスを送信すると、AWS Lambda@Edge 関数に委任された API コールが発生します。

API コールは、以下のステップで AWS Lambda@Edge 関数 (ビューアリクエスト) を呼び出します。

  • 正規表現でメールの形式が正しいか確認する
  • メールドメインが特定の許可されたドメインに属しているか確認する ( 形式を指定して @domain .tld )。このリストはコードに埋め込まれているか、AWS Systems Manager パラメータストアから取得される。このブログ投稿では、AWS Systems Manager パラメータストアを使用している。提供されているコードサンプルを使用する場合は、AWS Lambda 関数ロールに次のアクセス権限を追加すること
    • SSM: getParameter
  • 有効期限がある制限付きコンテンツ用の署名付き URL を作成する。署名付き URL には /auth パスが含まれる
  • 署名付き URL を含むメールを作成する

このブログ記事において、コードスニペットではロギングと例外処理を省略していますが、エラーを適切にログに記録して処理することをお勧めします。

完成した AWS Lambda 関数は次のようになります。

const AWS = require('aws-sdk');
const ses = new AWS.SES({ region: '<your-region>' });
const ssm = new AWS.SSM({ region: '<your-region>' });

// Either defined as a constant or retrieved from AWS Systems Manager Parameter Store
const SENDER = '<your-sender-email-address>';

// Either defined as a constant or retrieved from AWS Systems Manager Parameter Store
const SIGNING_URL = '<your-cloudfront-url>';

const signingUrl = `https://${SIGNING_URL}/auth`;

const content = `
<\!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Successful request</title>
  </head>
  <body>
    <p>Email with authentication token sent</p>
  </body>
</html>
`;

const response = {
    status: '200',
    statusDescription: 'OK',
    headers: {
        'cache-control': [{
            key: 'Cache-Control',
            value: 'max-age=100'
        }],
        'content-type': [{
            key: 'Content-Type',
            value: 'text/html'
        }]
    },
    body: content,
};

const error = {
    body: 'Email is not valid',
    bodyEncoding: 'text',
    headers: {
        'content-type': [{
            key: 'Content-Type',
            value: 'text/html'
        }]
    },
    status: '204',
    statusDescription: 'Error'
};

const cache = {}

const loadParameter = async(key, withDecryption = false) => {
    const { Parameter } = await ssm.getParameter({ Name: key, WithDecryption: withDecryption }).promise();
    return Parameter.Value;
};

const validateEmail = (allowedDomains, email) => {
    if (!allowedDomains) return false;
    const re = /\S+@\S+\.\S+/;
    return re.test(email) && allowedDomains.indexOf(email.substring(email.indexOf("@"))) >= 0;
};

const sendEmail = async(publicKey, privateKey, email) => {
    const cloudFront = new AWS.CloudFront.Signer(publicKey, privateKey);
    const signedUrl = cloudFront.getSignedUrl({
        url: signingUrl,
        expires: Math.floor((new Date()).getTime() / 1000) + (60 * 60 * 1) // Current Time in UTC + time in seconds, (60 * 60 * 1 = 1 hour)
    });

    const params = {
        Destination: {
            ToAddresses: [
                email
            ]
        },
        Message: {
            Body: {
                Html: {
                    Data: signedUrl,
                    Charset: 'UTF-8'
                }
            },
            Subject: {
                Data: '[stars on AWS] Login credentials for ' + email,
                Charset: 'UTF-8'
            }
        },
        Source: SENDER
    };
    await ses.sendEmail(params).promise();
};

exports.handler = async(event, context, callback) => {
    if (cache.allowedDomains == null) cache.allowedDomains = loadParameter('allowedDomains')
    if (cache.publicKey == null) cache.publicKey = loadParameter('publicKey');
    if (cache.privateKey == null) cache.privateKey = loadParameter('privateKey', true);

    const { allowedDomains, publicKey, privateKey } = cache;

    const request = event.Records[0].cf.request;
    if (request.method === 'GET') {
        const parameters = new URLSearchParams(request.querystring);
        if (parameters.has('email') === false) return error;
        const email = parameters.get('email');
        if (!validateEmail(allowedDomains, email)) return error;
        else {
            await sendEmail(publicKey, privateKey, email);
            return response;
        }
    }
    return error;
};
JavaScript

AWS Lambda@Edge 関数のメインハンドラーは、関連するパラメータ化されたキーをすべて取得します。

  • 許可されているメールドメインのリスト
  • URL に署名するための公開鍵と秘密鍵のペア

許可されたメールドメインリストは、AWS Systems Manager Parameter Store に StringList として保存されます。機密情報のハードコーディングはアンチパターンです。URL に署名するためのパブリックキー ID とプライベートキー (前提条件として作成) を AWS Systems Manager パラメータストアに保存します。パブリックキー ID は String として格納され、プライベートキーは SecureString として格納されます。

以下のコマンドを使用して、AWS Systems Manager パラメータストアにパブリックキー ID を保存します。

aws ssm put-parameter --name "publicKeyId" --type String --value "<your-public-key-id>" --region us-east-1

以下のコマンドを使用して、AWS Systems Manager パラメータストアにプライベートキーを保存します。

aws ssm put-parameter --name "privateKey" --type SecureString --value "$(cat <private-key-file>.pem)" --region us-east-1

関連するすべてのパラメーターをロードした後、リクエストが GET メソッドの場合、ハンドラーメソッドはクエリ文字列を解析します。まず、入力したメールアドレスがクエリ文字列に含まれます。ハンドラーはメールアドレスを検証します。次に、メールを正規表現と照合してテストし、メールが特定の許可されたドメインに属しているかどうかを確認します。最後に、すべての検証ステップが成功すると、署名付き URL が作成されます。

AWS SDK には、Amazon CloudFront の URL に署名するメカニズムが用意されています。このメソッドは、署名する URL と有効期限の 2 つの重要なパラメーターを取得します。署名付き URL は E メールに含まれ、受信者に送信されます。署名付き URL を第三者に共有することを軽減するには、有効期限を 1 時間から 2 分に減らします。署名付き URL を Amazon DynamoDB テーブルに保存し、その署名付き URL が使用されているかどうかを確認します。受信者のメールアドレスは API 呼び出しのクエリ文字列の一部として含まれます。Amazon CloudFront の TTL は、有効期限の値に設定する必要があります。これにより、署名付き URL の有効期限が切れた後、ユーザーはキャッシュされたファイルにアクセスできなくなります。

Amazon Simple Email Service で、送信者として使用されるメールアドレスを確認します。メールを受信したら、署名付き URL を使用してセキュリティで保護されたコンテンツにアクセスします。Amazon CloudFront を使用するには、AWS Lambda @Edge 関数からの特定のレスポンスが必要です。したがって、正常に処理された場合のレスポンスと、誤った動作を示すエラーオブジェクトを指定する必要があります。

おわりに

このブログ記事では、静的な Web サイトを保護するための認証メカニズムについて説明しました。署名付き URL や署名付き Cookie などの機能を活用して、匿名ユーザーが制限されたコンテンツにアクセスできないようにすることで、ユーザーを管理せずにユーザーを認証する方法を学習しました。署名付き URL は特定の期間のみ有効ですが、署名付き URL は第三者と共有できます。URL を共有するリスクを軽減するには、有効期限を 1 時間から 5 ~ 10 分に減らします。ユーザーが署名付き URL を既に使用しているかどうかを確認するプロセスを実装します。これは AWS Lambda@Edge を使って行うことができます。AWS Lambda は、Amazon DynamoDB テーブルから既に作成された署名付き URL をチェックします。この追加チェックにより、署名付き URL が有効なときに複数回使用されることはありません。

このブログの第 2 部では、制限されたコンテンツにアクセスするための適切なアクセス権をユーザーに付与する認証メカニズムの実装方法について説明します。第 2 部でソリューションを完成させます。

ご質問やご提案がございましたら、コメントを残してください。

Aleksandar Tolev
Aleksandar Tolev は、アマゾンウェブサービスのソリューションアーキテクトマネージャーで、製造業と自動車のお客様に情熱を注いでいます。技術的なガイダンスと信頼できるアドバイザーとして、自動車業界のお客様のクラウドジャーニーを支援しています。AWS に入社する前は、自動車 OEM のクラウドアーキテクトとして働いていました。

Marco Staufer
Marco Staufer は、アマゾンウェブサービスのグローバルアカウント担当者で自動車業界のお客様向けに活動しています。

翻訳はパートナーサクセスソリューションアーキテクト小林が担当しました。原文はこちらをご覧ください。