Amazon Web Services ブログ

Lambda@Edge を利用した外部サーバによる認可の実装

はじめに

このブログでは、 Lambda@Edge 利用し、リクエストに含まれるデータを外部の認証サーバーへ転送することによって、Amazon CloudFront でリクエストを認可する方法を説明します。ここでは、このようなワークフローでのリクエストの順序、 Node.js のサンプルコードによる実装手順、ヘッダーベースの認可の動作テストのために利用するシンプルな外部認証サーバー用の CloudFormation テンプレートの概要を説明します。また、このブログでは追加で最適化や考慮事項についても案内します。

ゲーム、ソフトウェア、ドキュメント、その他の大容量ファイルのダウンロードなど、コンテンツを CloudFront で安全に配信するには、特にアクセスが特定の視聴者に制限されている場合や、コンテンツが課金の対象になっている場合は、リクエストの承認が必要になることがよくあります。リクエストを承認する一般的な方法には、署名付き URL署名付き CookieJSON Web トークン (JWT)OpenID Connect (OIDC) がありますが、場合によっては、ヘッダーベースの認証など、他の既存の認証の方法でユーザーを認可する必要があります。

このソリューションが役立つような一般的な事例はいくつもあります。一つは、オンプレミス環境から AWS クラウドへウェブサーバーやストレージをマイグレーションする必要があるが、認可の仕組みを他のシステムやワークフローと共有しているため、既存の認可の仕組みは維持したいといったものです。別の例では、ダウンロードのワークフローとして追加またはバックアップの CDN として CloudFront を入れる際に、既存の CDN の実装で既にリモート認証のサーバーを利用して、リクエストを認可しているというものです。また、組み込みソフトウェアを利用したデバイスのソリューションでは、ソフトウェアアップデートによって、認可の仕組みを変更することは難しい、または不可能な場合があります。

外部サーバーによる認可フロー例の概要

次のダイアグラムは、Lambda@Edge を使用して既存の認可メカニズムを維持したまま、ダウンロードとストレージのワークフローを CloudFront と S3 に移行する例を示しています。

マイグレーション前 :

図1  : マイグレーション前

オンプレミス上の既存の認可を維持したまま、 CloudFront と S3 にマイグレーションした後 :

図2 : マイグレーション後

  1. エンドユーザーが Amazon CloudFront へ認証ヘッダー付きの HTTP GET リクエストを送信する
  2. CloudFront はビューワーリクエストで Lambda@Edge function をトリガーする
  3. Lambda@Edge function は認証ヘッダーを解析して、認証ヘッダー付きの HTTP リクエストを外部の認証サーバーへ送信する
  4. 認証情報が正当であれば外部の認証サーバーは、200 のステータスコードを返信するが、もし認証情報が正当でなければ403 のステータスコードを返信する
  5. Lambda@Edge function が外部の認証サーバーから 200 のステータスコードを受け取って、ファイルがキャッシュに存在する場合、CloudFront はエンドユーザーに 200 OK のステータスと一緒にコンテンツを返す。もし、ファイルがキャッシュに存在しない場合、次のように動作する :
    • a. CloudFront はコンテンツを取得するために S3 オリジンにリクエストを送信する
    • b. S3 オリジンは CloudFront にコンテンツを返す
    • c. CloudFront はエンドユーザーに 200 OK のステータスと一緒にコンテンツを返す
  6. Lambda@Edge function が外部の認証サーバーから 403 のステータスコードを受け取った場合、Lambda@Edge function はエンドユーザーに 403 のステータスのレスポンスを返す

Lambda@Edge のサンプルコード

次のコードは、Lambda@Edge で外部サーバーによる認可を実装する方法を示しています。これは Node.js HTTP モジュールを利用してHTTP コールを行います。

'use strict';

const https = require('https');
const keepaliveAgent = new https.Agent({keepAlive: true});

//Error message 
const content = `
<\!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Authentication Error</title>
  </head>
  <body>
    <p>Authentication Error</p>
  </body>
</html>
`;
exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;
    var authHeader = '';

//Get the authentication header from the request to CloudFront
    if (headers["authentication"]){
       authHeader = headers["authentication"][0].value;
    }
    const requestHeaders = {
        'authentication': authHeader
    };    
//Sends an HTTPS GET request that includes the authentication header 
//to the external authorization server 
    let req = https.request({
        hostname: '<External Server>',
        port: 443,
        path: '<path to authorization check URI>',
        method: 'GET',
        agent   : keepaliveAgent,
        headers : requestHeaders
    }, httpResponse => {  //Receives the response code from the external authorization server
        var data = '';
        httpResponse
            .on('error', e => {
                console.log('Error trace: ', e);
                console.log('Full response: ', httpResponse);
                req.destroy();
                callback(null, request);
            })
            .on('data', d => {
                data += d;
                console.log('data: ', data);
            })
            .on('end', () => {
//If the response code from the external server is not 200, 
//return a 403 response with the error message as the response body
                if (httpResponse.statusCode != '200') {
                    console.log(`statusCode: ${httpResponse.statusCode}`);
                    let response = {
                        status            : '403',
                        statusDescription : 'HTTP Forbidden',
                        body: content
                    };
                    req.destroy();
                    callback(null, response);
                    console.log(response);
                    return;
                }
//If the response code from the external server is 200, serve content to the viewer
                else{
                    req.destroy();
                    callback(null, request);
                    return;
                }
            });
        });
    req.end();
};

CloudFront ディストリビューションの作成

最初に、S3 をオリジンとする CloudFront ディストリビューションを作成します。オリジンリクエストポリシーで認可用に必要なヘッダー名を追加する必要があることに注意してください。ヘッダーを追加することで、Lambda@Edge がエンドユーザーからのリクエストから認証用のヘッダーを取得することができます。認証用のヘッダーは、キャッシュキーに含まれるべきではないので、キャッシュポリシーでヘッダーを追加する必要はありません。
もし、認可プロセスが特定のコンテンツだけで必要ならば、特定のコンテンツ用のパスだけをもつ CloudFront ビヘイビアに Lambda@Edge 関数を関連付けできます。ディストリビューションの作成について詳しくは、こちらをご覧ください。

図3 : オリジンリクエストの設定

Lambda@Edge のコードを作成して構成する

Lambda コンソール上で上記のコードをデプロイして、その関数を作成済みの CloudFront ビヘイビアに関連付けます。( Lambda@Edge 関数を CloudFront に関連づけるために、バージニアリージョンを選択する必要があります。) この関数はビューワーリクエストで動作するので、Cloudfront イベントのトリガーに「ビューワーリクエスト」を選択します。Lambda@Edge 関数のデプロイについて詳しくは、こちらをご覧ください。

図4 : Lambda@Edge へのデプロイ

シンプルな外部認証サーバーを用いたテスト

上記のワークフローでは、検証が成功すると外部認証サーバーは「200 OK」を返し、失敗の場合は「403」を返します。以下は、クイックなテスト目的で利用できる外部認証サーバー用のシンプルな PHP コードです。

<?php
$headers = getallheaders();
$authheader = $headers['authentication'];

if ($authheader){
  if ($authheader == "xxxxxxx") { 
    header("HTTP/1.1 200 OK");
    header("Status: 200");
    header("Content-Type: text/plain");
  }else{
    header("HTTP/1.1 403 ");
    header("Status: 403");
    header("Content-Type: text/plain");
  }
}else{
  header("HTTP/1.1 403 ");
  header("Status: 403");
  header("Content-Type: text/plain");
}
?>

例えば、curl コマンドを利用して以下のようなテストを実行できます。

200 OK :

$ curl -i https://xxxxxxxxxxxxx.cloudfront.net/ -H "authentication: xxxxxxx"
HTTP/2 200
content-type: text/html; charset=UTF-8
content-length: 14
date: Wed, 26 May 2021 13:53:39 GMT
server: Apache
last-modified: Wed, 14 Apr 2021 07:29:51 GMT
etag: "e-5bfe9b659bc1c"
accept-ranges: bytes

403 Forbidden :

$ curl -i https://xxxxxxxxxxxxx.cloudfront.net/ -H "authentication: <Invalid Value>"
HTTP/2 403
content-length: 179
server: CloudFront
date: Wed, 26 May 2021 13:54:40 GMT
x-cache: LambdaGeneratedResponse from cloudfront

CloudFormation を利用してテスト用の外部認証サーバーをデプロイする

このセクションでは、サンプルとなる CloudFormation テンプレートを用いて、PHP コードを利用したテスト用の外部認証サーバーをデプロイする方法を説明します。各手順を完了するためには、AWS accountAmazon Virtual Private Cloud ( Amazon VPC ) 、Amazon EC2 といった AWS リソースにアクセスできる IAM user を必要とします。この CloudFormation テンプレートは、SSL 証明書のデプロイを含んでいません。上記の Lambda@Edge のサンプルコードは、HTTPS 443 ポート経由で外部認証サーバーに接続するので、このテンプレートでテストする時は、サンプルコード上で外部認証サーバーへの接続を http:80 に変更するか、SSL 証明書を外部認証サーバーに追加してください。

  1. CloudFormation をデプロイするために をクリックしてください。
  2. デフォルトでは、CloudFormation コンソール内の「スタックのクイック作成」ページへ誘導され、CloudFormation テンプレートが自動で入力されます。

図5 : スタックの作成

  1. スタック名や必須のパラメーターはデフォルト値が入力されています。私たちのデモ環境では、「ExternalAuthTestServer」というスタック名になっています。もし、テストで異なる値を利用したい場合、認証ヘッダーの「AuthHeaderName」や「AuthHeaderValue」の値やパラメーターを変更できます。「スタックの作成」をクリックして続行します。

テンプレートをデプロイするとスタックの出力タブで、ヘッダー名( ExternalAuthHeaderName )、ヘッダーの値( ExternalAuthHeaderValue ) や認可用の URL を確認できます。

図6 : スタックの出力タブ

補足 : 最適化と考慮事項

  • HTTP メソッド : 外部認証サーバーへのアクセスでは GET メソッドの代わりに、POST メソッドを利用できます。
  • パーシステントコネクション : トラフィックに応じて、パーシステントコネクションの max socket を構成できます。
  • エラーハンドリング( フェールオープンとフェールクローズの検討 ) : Lambda@Edge が外部サーバーからレスポンスを得られない場合に、フェールオープンまたはフェールクローズの実施について検討する必要があります。
  • スケーリングの制限 : Lambda@Edge は秒間あたりのリクエスト数や同時実行数にはクォーターがあります。ビューワーリクエストで大量のリクエストが見込まれる場合、上限の引き上げが可能です。Lanbda@Edge のクォーター
  • 追加ヘッダーの検証 : 強力なセキュリティの追加を検討する場合、外部サーバーへヘッダーを送付する前に、共有シークレットによるハッシュキーを利用してリクエストヘッダーの検証を追加できます。Lambda@Edge で取得される共有シークレットを保存するために AWS Secrets Manager を利用できます。詳細はこのブログを参照してください。
  • コスト : このソリューションのコストは、予想されるビューワーリクエストの数を見積り、またテストにて関数の実行時間を確認してください。関数のプロセスに外部ネットワークがあるので、関数の実行時間は外部サーバーの処理やネットワークのレイテンシーに依存します。加えて、CloudWatch Logs のコストがあり、アクセス数からログのボリュームや関数からログの出力量を見積もります。Lambda@Edge のコストを見積もるベストプラクティスを確認してください。

クリーンアップ

CloudFormation テンプレートを使用してサンプルの外部認証サーバーをデプロイした場合、CloudFormation テンプレートでローンチしたリソースに関するコスト増を避けるため、テスト後に CloudFormation スタックを削除してください。

おわりに

このブログでは、ビューワーリクエストをトリガーした Lambda@Edge を通じて外部認証サーバーを利用した認可の実装方法について説明しました。これは、コンテンツデリバリーである CloudFront プラットフォームのパフォーマンス、可用性やセキュリティの恩恵を受けながら、既存の認可方法を活用したり高度にカスタマイズされたエンドユーザーの認可ソリューションを開発、構築する柔軟性があります。

他の Lambda@Edge CDN ワークフローのカスマイズについてさらに学ぶために、Lambda@Edge でのカスタマイズに関するドキュメントを参照してください。

本ブログは、External Server Authorization with Lambda@Edge を翻訳したものです。翻訳は、PSSA 小林が担当しました。