目的
PoC用Webアプリケーションを開発した際に、アプリケーションを改修することなくユーザー認証機能を追加する必要がありました。要件は短期間・低コストで実装すること、そして認証したユーザーのメールアドレスを後段のアプリケーションで受け取ることができるようにすることです。
このような制約で実装できる構成を検討したところ、アプリケーション側に認証を実装するのではなく、インフラレイヤーで認証を実装する方法を採用しました。
構成検討
今回、認証を実装するにあたり下記3つの方法を検討しました。
アプリでの認証実装
この方法はインフラ側をシンプルに保つ事ができますが、今回の制約では採用できない方式でした。
ALB authenticate-cognito
この方法も、アプリケーションへの改修なく認証機能を実装できる方法です。ただ、今回はドメイン取得や証明書の用意が難しかったため、採用できませんでした。
CloudFront + Lambda@Edge + Cognito【採用】
この方法であれば、ドメイン取得や証明書の用意をせずにhttpsでの通信と必要な認証を実施できるため、採用しました。
使用サービス
CloudFront
各ロケーションからのユーザーアクセスを高速化し、オリジンへの負荷軽減を目的に使用されるAWSサービスです。今回はCloudFrontドメイン(*.cloudfront.net)をそのまま使用することを許容し、https通信を実現するためにCloudFrontを使用しました。
WAFを簡単に組み込むこともできるため、よりセキュアな構成を実現することもできます。今回は社内からの利用のみであったため、社内IPでのIP制限と一般的なWAFルールを適用しています。
Cognito
Cognitoを使用することでユーザー登録や初期パスワードの発行、および構築済み環境への実装を容易に実施することができます。
Lambda@Edge
アプリケーションレイヤーへの修正を加えずに、Cognito認証を実装するために使用しました。
概要
AWS CognitoとCloudFrontを使って、インフラレイヤーで認証機能を簡単に実装することができます。CloudFrontへアクセスした際にCognitoで認証されるよう、CloudFrontとLambdaを連携させます。
手順
1. CloudFront Distribution作成
こちらをご参照ください:https://qiita.com/hirotaka_s/items/41bdaedb07376798089b
CloudFrontを定額フリープランで使用してしまうとLambda@Edgeを実装することができないため、ご注意ください。
2. Cognito ユーザープール作成
aws cognito-idp create-user-pool --pool-name my-app-pool
※出力のIDをメモしておきます。
3. Cognito アプリクライアント作成
ログイン後の戻り先(Callback URL)にCloudFrontのURLを指定します。
※\$POOL_IDを手順2で作成したプールのIDに、\$CF_URLを手順1で作成したCloudFrontのURLに置き換えてください。
aws cognito-idp create-user-pool-client \
--user-pool-id $POOL_ID \
--client-name cf-auth-client \
--explicit-auth-flows "ALLOW_REFRESH_TOKEN_AUTH" "ALLOW_USER_PASSWORD_AUTH" \
--allowed-o-auth-flows "code" \
--allowed-o-auth-scopes "openid" "email" "profile" \
--callback-urls "https://$CF_URL" \
--supported-identity-providers "COGNITO" \
--allowed-o-auth-flows-user-pool-client
※出力されるUserPoolIdとUserPoolClientをメモしておきます。
4. Cognito ログイン用ドメイン作成
※\$POOL_IDを手順2で作成したプールのIDに置き換えてください。
aws cognito-idp create-user-pool-domain \
--user-pool-id $POOL_ID \
--domain my-auth-domain-$(date +%s)
5. Lambda 信頼ポリシー作成
cat << 'EOF' > edge-trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]
},
"Action": "sts:AssumeRole"
}
]
}
EOF
# ロールの作成
ROLE_ARN=$(aws iam create-role \
--role-name EdgeAuthRole \
--assume-role-policy-document file://edge-trust-policy.json \
--query 'Role.Arn' --output text)
# 基本的な実行権限を付与
aws iam attach-role-policy \
--role-name EdgeAuthRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
6. Lambda 作成
Cognitoで使用されるドメインを確認します。
DOMAIN_PREFIX=$(aws cognito-idp describe-user-pool --user-pool-id $POOL_ID --query "UserPool.Domain" --output text)
YOUR_DOMAIN="${DOMAIN_PREFIX}.auth.ap-northeast-1.amazoncognito.com"
echo "Domain: $YOUR_DOMAIN"
Lambdaコードを作成します。
※13行目あたりにある、\$POOL_IDを手順2で作成したプールのIDに、\$CLIENT_IDを手順3で確認したUserPoolClientに、\$YOUR_DOMAINを上記で確認した値に、それぞれ置き換えてください。
# 作業用フォルダ作成
mkdir edge-auth && cd edge-auth
# ライブラリのインストール
npm install cognito-at-edge
# Lambdaコードの作成(お手本ベース)
cat << 'EOF' > index.mjs
import { Authenticator } from 'cognito-at-edge';
const authenticator = new Authenticator({
region: 'ap-northeast-1',
userPoolId: '$POOL_ID',
userPoolAppId: '$CLIENT_ID',
userPoolDomain: '$YOUR_DOMAIN',
cookieExpirationDays: 1,
parseAuthPath: '/',
});
export const handler = async (event) => {
return await authenticator.handle(event);
};
EOF
# node_modulesも含めてZIP化
zip -r function.zip index.mjs node_modules
作成したコードをデプロイするLambdaサービスを作成します。
CloudFrontと連携するため、バージニア北部リージョンで作成する必要があります。
aws lambda create-function \
--function-name EdgeCognitoAuth \
--runtime nodejs20.x \
--role $ROLE_ARN \
--handler index.handler \
--zip-file fileb://function.zip \
--region us-east-1
Lambdaのバージョンを発行します。
aws lambda publish-version \
--function-name EdgeCognitoAuth \
--region us-east-1
7. CloudFrontとLambdaの関連付け
現在の設定を取得します
※\$DIST_IDを手順1で作成したCloudFrontのDistributionIDに置き換えてください。
DIST_DATA=$(aws cloudfront get-distribution-config --id $DIST_ID)
DIST_ETAG=$(echo $DIST_DATA | jq -r '.ETag')
現在の設定をもとに、作成したLambdaへの関連付けを設定します。
※下から3行目の\$DIST_IDを手順1で作成したCloudFrontのDistributionIDに置き換えてください。
NEW_CONFIG=$(echo $DIST_DATA | jq -c --arg arn "$LAMBDA_VER_ARN" '
.DistributionConfig |
.DefaultCacheBehavior.LambdaFunctionAssociations = {
"Quantity": 1,
"Items": [
{
"LambdaFunctionARN": $arn,
"EventType": "viewer-request",
"IncludeBody": false
}
]
}
')
aws cloudfront update-distribution \
--id $DIST_ID \
--if-match $DIST_ETAG \
--distribution-config "$NEW_CONFIG"
8. 動作確認
手順1で作成したCloudFront DistributionのURLにアクセスすると、Cognitoの認証画面に飛びます。

ログイン画面は組み込みのものを使用するとカスタマイズにあまり柔軟性が無いため、画面を重視する用途には向きません。
9. Cognito ユーザー作成
※$POOL_IDを手順6で確認したPool_IDに、test@example.comを任意のメールアドレスに置き換えてください。
aws cognito-idp admin-create-user \
--user-pool-id $POOL_ID \
--username test-user \
--user-attributes Name=email,Value=test@example.com
デフォルトのメールテンプレートを使用すると最後のピリオドがパスワードの一部に見えてしまう可能性があるため、htmlで編集できるのでアプリケーションごとの適切なメッセージとなるよう修正をおすすめします。
10. ログイン検証
ログイン情報を入力すると強制パスワード変更の画面に移動します。

パスワードを変更すると、無事Webサーバーにアクセスできました。

まとめ
Cognito、CloudFront、Lambda@Edge、WAFを組み合わせることで、アプリケーションレイヤーに手を加えることなく簡単にセキュアな認証機能を実装することができました。
PoCなどではスピードを優先することも多いと思いますが、このような構成であればアジリティを損なうことなくセキュアなアプリケーションの入り口を作ることができます。
参考になれば幸いです。

