👌

CloudFront で Lambda@Edge を使って OpenID Connect (OIDC) 認証する

2024/02/11に公開

概要

CloudFront と S3 でスタティックなウェブサイトを構築し、外部の認証プロバイダ (IdP) を使って認証をおこなう方法です。

AWS によるサンプルスタック aws-samples/lambdaedge-openidconnect-samples を使用して構築します。ここでは例として Google を IdP とし、特定のメールアドレスのユーザーのみ参照できるようにする手順について記述しています。

このスタックによりデプロイされるリソース構成は以下のようになります。Lambda 関数が使用する設定は手動で Secrets Manager に作成します。

リソース構成

OpenID Connect による認証シーケンス

CloudFront にアクセスした際のシーケンスは以下のようになります。

Lambda 関数では、以下の 2 つの処理をおこないます。

  • サイトへのリクエスト毎に認証済みかどうかをチェックし、未認証であれば IdP にリダイレクトする
  • 認証が成功するとコールバック URL にリダイレクトされるので、IdP からアクセストークンを取得して Cookie に設定する

構築手順

前提条件

  • AWS SAM CLI がインストール済み

1. Secret を作成

認証 Lambda 関数で使用する Secret を Secrets Manager に作成します。(いったん空の内容で作成します。)

$ SECRET_NAME=cloudfront-oidc-configuration
$ aws secretsmanager create-secret --region us-east-1 \
    --name "${SECRET_NAME}" --secret-string "{}"
{
    "ARN": "arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:cloudfront-oidc-configuration-XXXXXX",
    "Name": "cloudfront-oidc-configuration",
    "VersionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

出力された ARN はこのあと使用します。

2. デプロイ

チェックアウト

$ git clone https://github.com/aws-samples/lambdaedge-openidconnect-samples
$ cd lambdaedge-openidconnect-samples

認証 Lambda 関数の修正

前の手順で作成した Secret の ARN を src/js/sm-key.txt に記述します。

$ SECRET_ARN=arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:cloudfront-oidc-configuration-XXXXXX
$ echo "${SECRET_ARN}" > src/js/sm-key.txt

スタックのデプロイ

2023/04 以降、デフォルトのバケット設定が変わりそのままではデプロイできなくなっているため、template.yaml を編集して LoggingBucket リソースに OwnershipControls プロパティを追加します。(2023/10 現在。修正されていたら不要です。)
参考) #57 Bucket cannot have ACLs set with ObjectOwnership's BucketOwnerEnforced

     LoggingBucket:
       Type: AWS::S3::Bucket
       Properties:
+        OwnershipControls:
+          Rules:
+            - ObjectOwnership: BucketOwnerPreferred
         AccessControl: LogDeliveryWrite
         BucketName: !Ref LogBucketName
         BucketEncryption:

以下のコマンドで CloudFormation スタックをデプロイします。

$ sam build
$ sam deploy --guided

Configuring SAM deploy
======================

    Looking for config file [samconfig.toml] :  Not found

    Setting default arguments for 'sam deploy'
    =========================================
    Stack Name [sam-app]: cloudfront-oidc-auth
    AWS Region [ap-northeast-1]: us-east-1
    Parameter BucketName []: xxxxxxxxxxxx
    Parameter LogBucketName []: xxxxxxxxxxxx
    Parameter SecretKeyArn []: arn:aws:secretsmanager:us-east-1:xxxxxxxxxxxx:secret:cloudfront-oidc-configuration-XXXXXX
    Parameter MinimumTLSVersion [TLSv1.2_2018]:

...
  • StackName: CloudFormation スタック名 (任意)
  • AWS Region: us-east-1 を指定
  • BucketName: サイトのコンテンツを格納する S3 バケット名 (スタックが作成します)
  • LogBucketName: CloudFront のアクセスログを格納する S3 バケット名 (スタックが作成します)
  • SecretKeyArn: IdP の設定ファイルが格納されている Secrets Manager の ARN (事前に作成済のものを指定)

これにより設定ファイル (samconfig.toml) が作成されます。設定を変更する場合はファイルを編集して sam deploy を実行することでスタックを更新できます。

3. 認証プロバイダ (IdP) の設定

IdP のクライアントを認証するための Client ID/Secret を払い出します。CloudFront で動作する Lambda 関数がクライアントという位置付けです。

Google の場合は以下の手順でおこないます。

  1. Google API Console にアクセス
  2. プロジェクトを作成
  3. 「OAuth consent screen」から
    • User Type: External
    • App name: 例: cloudfront-auth
  4. 「Credential」からクライアントを作成
    1. 「Create credentials」→「OAuth client ID」
    2. 「Application type」に「Web application」を選択
    3. 「Name」に適当な名前を入力 (例: cloudfront-auth)
    4. 「Authorized redirect URIs」に「Callback URL」を入力
    5. 「Client ID」と「Client secret」が発行されるので、メモしておく

Callback URL には IdP からの認証結果をリダイレクトしする URL として、ホストの /_callback パスを指定します。(例: https://xxxxxxxxxxxx.cloudfront.net/_callback)

Google API Credentials 一覧画面

4. IdP 設定を作成して設定

認証 Lambda 関数で使用する IdP 設定ファイルを作成します。

--cloudfront_host には公開するサイトのドメインを指定します。カスタムドメインを割り当てず CloudFront のデフォルトドメインを使用する場合は、スタックが作成した CloudFront Distribution のドメイン名を確認してください。

$ cd cli
$ pip install -r requirements.txt
$ CLIENT_ID=(Client ID)
$ CLIENT_SECRET=(Client Secret)
$ CLOUDFRONT_HOST=(サイトのドメイン名)
$ IDP_DOMAIN_NAME=accounts.google.com
$ python cli.py \
    --client_id "${CLIENT_ID}" \
    --client_secret "${CLIENT_SECRET}" \
    --cloudfront_host "${CLOUDFRONT_HOST}" \
    --idp_domain_name "${IDP_DOMAIN_NAME}"

これにより、設定ファイルが cloudfront_config_rendered.json に出力されます。またこれを Base64 エンコードして Secrets Manager に登録する形式が encoded_cloudfront_config_rendered.json に出力されるため、これを使って登録します。

$ SECRET_NAME=cloudfront-oidc-configuration
$ aws secretsmanager update-secret --region us-east-1 \
    --secret-id "${SECRET_NAME}" \
    --secret-string "$(cat encoded_cloudfront_config_rendered.json)"

5. アクセス可能なユーザーを制限

認証を Google でおこなうようにしても Google アカウントを持っているすべてのユーザーがアクセスできてしまっては意味がありません。特定のユーザーのみアクセス許可するようにするには、ソース (src/js/auth.js) を修正してデプロイし直します。

ユーザーのメールアドレスは JWT トークンの sub フィールドに含まれています。このフィールドをチェックして特定のユーザーのみアクセスを許可するようにします。

async function getVerifyJwtResponse(request, headers) {
    try {
        const jwt = await verifyJwt(Cookie.parse(headers.cookie[0].value).TOKEN, config.PUBLIC_KEY.trim(), {
            algorithms: ['RS256']
        });
        if (jwt["sub"] !== "yh1224@gmail.com") {
            return getUnauthorizedPayload('Unauthorized.', `User is not permitted`, '');
        }
        return request;

FAQ

認証がうまくいかない場合、CloudWatch Logs の /aws/lambda/us-east-1.(スタック名)-CloudFrontAuthFunction-XXXX を参照してみてください。(このログはアクセス元のリージョン別に作成されます。)

Discussion