CloudFront で Lambda@Edge を使って OpenID Connect (OIDC) 認証する
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 の場合は以下の手順でおこないます。
- Google API Console にアクセス
- プロジェクトを作成
- 「OAuth consent screen」から
- User Type: External
- App name: 例:
cloudfront-auth
- 「Credential」からクライアントを作成
- 「Create credentials」→「OAuth client ID」
- 「Application type」に「Web application」を選択
- 「Name」に適当な名前を入力 (例:
cloudfront-auth
) - 「Authorized redirect URIs」に「Callback URL」を入力
- 「Client ID」と「Client secret」が発行されるので、メモしておく
Callback URL には IdP からの認証結果をリダイレクトしする URL として、ホストの /_callback
パスを指定します。(例: https://xxxxxxxxxxxx.cloudfront.net/_callback
)
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