0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudFront+S3+Cognitoで認証付き静的サイトを構築(前半)

Posted at

1.はじめに

1.1.構築の背景

個人開発している静的サイトを会社メンバだけに公開させようと思い、Cognito と Lambda@Edgeを利用した 認証機能を実装しようと思いましてハンズオンをしてみました。

ボリュームが大きいため、前半に**「CloudFront+S3+Cognito」の構築、後半で「Lambda@Edgeを利用した認証機能」**を実装しようと思います。

1.2.前提知識

本記事は以下の知識をお持ちの方を対象としています:

  • AWS基本サービス(S3、CloudFront、Lambda)の基本的な理解

1.3.アーキテクチャ方針

  • 前半部分:CloudFront + S3 + Cognito を用いた認証基盤の構築
    • User Poolのみ使用(Identity Poolは使用しない)
      • Lambda@Edge による認証が通れば CloudFront から S3へアクセスが可能なため

  • 後半部分:Lambda@Edgeから、JWT検証ロジックを持つLambda関数を呼び出す構成
    • Node.js の場合、以下ライブラリがあり導入は容易(ブログも多い)ですが、学習目的のためPythonを利用

1.4.アーキテクチャ図

1.4.1.最終的な構成図

最終 アーキテクト図

2.ハンズオン

2.0.構築方法

2.0.1.構築環境

  • 以下環境より、CLIを利用して構築
  • 後半構築の Lambda@Edgeはus-east-1で作成する必要があり、リージョンを合わせるため選択
環境 設定
環境 AWS CloudShell
リージョン バージニア北部(us-east-1)

2.0.2.ファイル構成

lambda-edge-cognito-auth/
├── lambda/
│   ├── pre-signup/         # 新規ログイン時のドメインチェック
│   └── edge/               # Lambda@Edge関数(※後半作成)
│   
├── frontend/
│   └── index.html
│   └── login.html
└── policy/
    └── bucket-policy.json

2.1.S3バケット作成

  • 方針
    • CloudFrontのOAC(オリジンアクセスコントロール)でS3にアクセス
    • ユーザは直接S3にアクセス不可

2.1.1.バケット作成

# フォルダ作成
mkdir -p lambda-edge-cognito-auth/{lambda,frontend,policy}
cd lambda-edge-cognito-auth/frontend

# 変数設定
BUCKET_NAME="lambda-edge-auth-$(date +%Y%m%d%H%M%S)"
REGION="us-east-1"
echo "バケット名: $BUCKET_NAME"

# S3バケット作成
aws s3 mb s3://$BUCKET_NAME --region $REGION

# 作成確認
aws s3 ls | grep lambda-edge-auth

2.1.2.ログイン後の静的画面作成

# ログイン後のページ作成
# index.html作成
cat > index.html << EOF
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>認証済みページ</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background-color: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 { color: #2c3e50; text-align: center; }
        .info {
            background-color: #e8f5e8;
            padding: 15px;
            border-radius: 5px;
            margin: 20px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>ようこそ!認証済みユーザー様</h1>
        <div class="info">
            <p><strong>認証成功</strong></p>
            <p>このページは認証済みユーザーのみアクセス可能です。</p>
        </div>
        <p>アクセス時刻: <span id="time"></span></p>
    </div>
    <script>
        document.getElementById('time').textContent = new Date().toLocaleString('ja-JP');
    </script>
</body>
</html>
EOF

上記で作成したindex.htmlをアップロード

# S3にアップロード
aws s3 cp index.html s3://$BUCKET_NAME/

2.2.CloudFront作成

2.2.1.CloudFrontのOAC作成

# OAC(Origin Access Control)作成
aws cloudfront create-origin-access-control \
  --origin-access-control-config '{
    "Name": "lambda-edge-auth-oac",
    "Description": "OAC for Lambda@Edge Auth Demo",
    "OriginAccessControlOriginType": "s3",
    "SigningBehavior": "always",
    "SigningProtocol": "sigv4"
  }'

# CloudFront作成時に利用するためにOAC IDを取得
OAC_ID=$(aws cloudfront list-origin-access-controls --query 'OriginAccessControlList.Items[?Name==`lambda-edge-auth-oac`].Id' --output text)
echo "OAC ID: $OAC_ID"

2.2.2.CloudFront作成

# CloudFrontディストリビューション作成
aws cloudfront create-distribution \
  --distribution-config '{
    "CallerReference": "'$(date +%s)'",
    "DefaultRootObject": "index.html",
    "Comment": "Lambda@Edge Auth Demo Distribution",
    "Enabled": true,
    "PriceClass": "PriceClass_All",
    "Origins": {
      "Quantity": 1,
      "Items": [
        {
          "Id": "S3-'$BUCKET_NAME'",
          "DomainName": "'$BUCKET_NAME'.s3.amazonaws.com",
          "S3OriginConfig": {
            "OriginAccessIdentity": ""
          },
          "OriginAccessControlId": "'$OAC_ID'"
        }
      ]
    },
    "DefaultCacheBehavior": {
      "TargetOriginId": "S3-'$BUCKET_NAME'",
      "ViewerProtocolPolicy": "redirect-to-https",
      "AllowedMethods": {
        "Quantity": 2,
        "Items": ["GET", "HEAD"],
        "CachedMethods": { "Quantity": 2, "Items": ["GET", "HEAD"] }
      },
      "ForwardedValues": {
        "QueryString": true,
        "Cookies": {
          "Forward": "whitelist",
          "WhitelistedNames": {
            "Quantity": 1,
            "Items": ["id_token"]
          }
        }
      },
      "MinTTL": 0,
      "DefaultTTL": 0,
      "MaxTTL": 0,
      "TrustedSigners": {
        "Enabled": false,
        "Quantity": 0
      }
    }
  }'

CloudFrontのドメイン名とディストリビューションIDを取得

# CloudFrontの情報取得
DISTRIBUTION_ID=$(aws cloudfront list-distributions --query 'DistributionList.Items[?Comment==`Lambda@Edge Auth Demo Distribution`].Id' --output text)
CLOUDFRONT_DOMAIN=$(aws cloudfront list-distributions --query 'DistributionList.Items[?Comment==`Lambda@Edge Auth Demo Distribution`].DomainName' --output text)

echo "Distribution ID: $DISTRIBUTION_ID"
echo "CloudFront Domain: $CLOUDFRONT_DOMAIN"

2.2.3.【S3】のバケットポリシー修正

# ディレクトリ移動
cd ../policy/

# S3バケットポリシー作成・適用
cat > bucket-policy.json << EOF
{
  "Version": "2008-10-17",
  "Id": "PolicyForCloudFrontPrivateContent",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipal",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::$BUCKET_NAME/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::$(aws sts get-caller-identity --query Account --output text):distribution/$DISTRIBUTION_ID"
        }
      }
    }
  ]
}
EOF

# バケットポリシー適用
aws s3api put-bucket-policy --bucket $BUCKET_NAME --policy file://bucket-policy.json

2.2.4.ここまでの作成確認

以下すべての変数が出力されることを確認

# 変数の確認
echo "S3バケット: $BUCKET_NAME"
echo "CloudFront URL: https://$CLOUDFRONT_DOMAIN"
echo "Distribution ID: $DISTRIBUTION_ID"
echo "OAC ID: $OAC_ID"

2.2.5.アクセス確認

2.2.5.1.CloudFront経由でのアクセス(成功)
# CloudFront経由でのアクセス(成功)
curl -I https://$CLOUDFRONT_DOMAIN

# レスポンス内容(一部抜粋)
HTTP/2 200 
2.2.5.2.S3に直接アクセス(失敗)
# S3に直接アクセス(失敗)
curl -I https://$BUCKET_NAME.s3.amazonaws.com/index.html

# レスポンス内容(一部抜粋)
HTTP/1.1 403 Forbidden
2.2.5.3.ブラウザでのアクセス確認

■ ブラウザでの画面

ブラウザでの画面


2.3.Cognito作成

  • 方針
    • 会社ドメイン制限(※ 2.4.1.ドメインチェック Lambdaにて実装)

■ 構成内容
以下フローでユーザは Cognito(ユーザプール)へのアクセスをする

  • フロント画面 → アプリケーションクライアント → ユーザプール
リソース名 役割
User Pool ユーザ情報を保存するデータベース
アプリケーションクライアント User Pool を操作するための設定

2.3.1.Cognito(ユーザプール)作成

# Cognito User Pool作成
aws cognito-idp create-user-pool \
  --pool-name "lambda-edge-auth-pool" \
  --policies '{
    "PasswordPolicy": {
      "MinimumLength": 8,
      "RequireUppercase": true,
      "RequireLowercase": true,
      "RequireNumbers": true,
      "RequireSymbols": false
    }
  }' \
  --auto-verified-attributes email \
  --username-attributes email \
  --schema '[
    {
      "AttributeDataType": "String",
      "Name": "email",
      "Required": true
    }
  ]'

# UserPool IDの取得
USER_POOL_ID=$(aws cognito-idp list-user-pools --max-results 20 --query 'UserPools[?Name==`lambda-edge-auth-pool`].Id' --output text)
echo "User Pool ID: $USER_POOL_ID"

2.3.2.Cognito(アプリケーションクライアント)作成

  • 今回はクライアントサイドのみで完結する実装のためimplicitフローを使用しますが、本番環境ではよりセキュアなAuthorization Code Grantフローの利用を推奨します
# アプリケーションクライアント作成
aws cognito-idp create-user-pool-client \
  --user-pool-id $USER_POOL_ID \
  --client-name "lambda-edge-auth-client" \
  --supported-identity-providers COGNITO \
  --callback-urls "https://$CLOUDFRONT_DOMAIN/index.html" \
  --logout-urls "https://$CLOUDFRONT_DOMAIN" \
  --allowed-o-auth-flows implicit \
  --allowed-o-auth-scopes openid email profile \
  --allowed-o-auth-flows-user-pool-client \
  --explicit-auth-flows ALLOW_USER_SRP_AUTH ALLOW_REFRESH_TOKEN_AUTH

# Client IDの取得
CLIENT_ID=$(aws cognito-idp list-user-pool-clients --user-pool-id $USER_POOL_ID --query 'UserPoolClients[?ClientName==`lambda-edge-auth-client`].ClientId' --output text)
echo "Client ID: $CLIENT_ID"

2.3.3.Cognitoドメイン作成(認証画面用)

# ドメイン名作成(ユニークにするため日付)
COGNITO_DOMAIN="lambda-edge-auth-$(date +%Y%m%d%H%M%S)"

# ドメイン作成
aws cognito-idp create-user-pool-domain \
  --domain $COGNITO_DOMAIN \
  --user-pool-id $USER_POOL_ID

# ドメイン作成確認
COGNITO_DOMAIN=$(aws cognito-idp describe-user-pool --user-pool-id $USER_POOL_ID --query 'UserPool.Domain' --output text)
echo "Cognito Domain: $COGNITO_DOMAIN"
echo "Cognito Login URL: https://$COGNITO_DOMAIN.auth.us-east-1.amazoncognito.com"

2.3.4.ここまでの作成確認

以下すべての変数が出力されることを確認

echo "User Pool ID: $USER_POOL_ID"
echo "Client ID: $CLIENT_ID"
echo "Cognito Domain: $COGNITO_DOMAIN"
echo "Login URL: https://$COGNITO_DOMAIN.auth.us-east-1.amazoncognito.com"

2.3.5.テストユーザ作成

# テストユーザー作成
aws cognito-idp admin-create-user \
  --user-pool-id $USER_POOL_ID \
  --username testuser@example.com \
  --user-attributes Name=email,Value=testuser@example.com Name=email_verified,Value=true \
  --temporary-password TempPass123! \
  --message-action SUPPRESS

# パスワード設定
aws cognito-idp admin-set-user-password \
  --user-pool-id $USER_POOL_ID \
  --username testuser@example.com \
  --password Password123! \
  --permanent

2.3.6.ログイン挙動確認

  • ここまで作成した Cognitoの画面からログインできた場合、CloudFrontへリダイレクト(S3のログイン後ページ)される
  • ここではログインのみ挙動を確認し、新規ユーザ(Sign up)については検証を実施しない。
2.3.6.1.ログインURL作成
# ログインURL作成
LOGIN_URL="https://$COGNITO_DOMAIN.auth.us-east-1.amazoncognito.com/login?client_id=$CLIENT_ID&response_type=token&scope=openid+email+profile&redirect_uri=https://$CLOUDFRONT_DOMAIN/index.html"
echo "Login URL: $LOGIN_URL"
2.3.6.2.テストユーザでログイン

テストログイン情報:

  • ユーザー名: testuser@example.com
  • パスワード: Password123!
ログインの画面
2.3.6.3.成功時の画面
  • 上記ログインが成功すると以下画面が表示される
ログイン成功
  • 成功時のURL
https://{CloudFront ドメイン名}/#id_token={IDトークン}&access_token={アクセストークン}&expires_in={有効時間}&token_type={トークンタイプ}
2.3.6.4.失敗時の画面
  • 上記ログインが失敗すると以下画面が表示される
ログイン失敗

2.4.Lambda作成

2.4.1.ドメインチェック Lambda

2.4.1.1.概要
  • 特定のドメインのメールアドレスのみ登録可能とする要件より実装

■ 構成内容
以下フローで ドメインチェックLambdaは ユーザのメールアドレスをチェックする

  • フロント画面 → 新規ユーザ作成 → ドメインチェックLambda自動実行して判定 → 登録可能なドメインであれば登録 / 登録できなければエラーを表示
2.4.1.2.ロジック作成

以下2つのメールアドレスのドメインの場合登録が可能とする

  • example.com
  • example.co.jp
# Lambda関数用ディレクトリ作成
cd ../lambda
mkdir pre-signup && cd pre-signup

# Lambda関数作成
cat > lambda_function.py << EOF
import json

def lambda_handler(event, context):
    """
    Cognito Pre Sign-up Trigger
    特定ドメインのメールアドレスのみ登録を許可
    """
    email = event['request']['userAttributes']['email']
    
    # 許可ドメインリスト
    allowed_domains = ['example.com', 'example.co.jp']
    
    domain = email.split('@')[1] if '@' in email else ''
    
    if domain not in allowed_domains:
        raise Exception('Domain not allowed') 
    
    # 自動確認設定
    event['response']['autoConfirmUser'] = True
    event['response']['autoVerifyEmail'] = True
    
    return event
EOF

# ZIP化
zip lambda_function.zip lambda_function.py
2.4.1.3.IAMロール作成
# IAMロール作成
aws iam create-role \
  --role-name lambda-cognito-execution-role \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "lambda.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
  }'

# 基本実行ポリシーアタッチ
aws iam attach-role-policy \
  --role-name lambda-cognito-execution-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
2.4.1.4.Lambdaデプロイ
# Lambda関数作成
aws lambda create-function \
  --function-name cognito-pre-signup-domain-check \
  --runtime python3.10 \
  --role arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):role/lambda-cognito-execution-role \
  --handler lambda_function.lambda_handler \
  --zip-file fileb://lambda_function.zip
2.4.1.5.Cognito(ユーザプール)にトリガー設定
# Cognito User PoolにTrigger設定
aws cognito-idp update-user-pool \
  --user-pool-id $USER_POOL_ID \
  --lambda-config '{
    "PreSignUp": "arn:aws:lambda:us-east-1:$(aws sts get-caller-identity --query Account --output text):function:cognito-pre-signup-domain-check"
  }'
2.4.1.6.Cognito(ユーザプール)にLambda関数を呼び出す実行権限付与
# CognitoがLambda関数を呼び出せるように権限追加
aws lambda add-permission \
  --function-name cognito-pre-signup-domain-check \
  --statement-id cognito-trigger-permission \
  --action lambda:InvokeFunction \
  --principal cognito-idp.amazonaws.com \
  --source-arn arn:aws:cognito-idp:us-east-1:$(aws sts get-caller-identity --query Account --output text):userpool/$USER_POOL_ID

# 権限確認
aws lambda get-policy --function-name cognito-pre-signup-domain-check
2.4.1.7.上記設定後のSign up 画面での挙動
# ログインURL作成
LOGIN_URL="https://$COGNITO_DOMAIN.auth.us-east-1.amazoncognito.com/login?client_id=$CLIENT_ID&response_type=token&scope=openid+email+profile&redirect_uri=https://$CLOUDFRONT_DOMAIN/index.html"
echo "Login URL: $LOGIN_URL"

■ Sign up 画面でテストを実施(赤枠部分がSign upになってることを確認)

  • 新規ユーザ登録時に、メールアドレスの ドメインを検証するため
Sign up 画面

■ 許可されていないドメインの場合

  • 許可されていないドメインで Sign up する際、入力形式のチェックは通りますが、、
Sign up 入力画面
  • ドメインチェックLambdaがトリガされ、ドメインに問題があるとエラーコードが表示される
Sign up 失敗
  • 出力されるLambdaのログ
    • raise Exception('Domain not allowed')でキャッチされてることが確認できる。
Lambdaログ失敗

■ 許可されているドメインの場合

  • ユーザを作成した場合、以下の画面にリダイレクト
ログイン成功
  • 出力されるLambdaのログ
    • Lambdaは実行されているが問題ないため、Errorログ等は出力されない
Lambdaログ成功

2.5.ログインページの作成と適用

2.5.1.ログインページの作成
# frontendディレクトリに移動
cd ../frontend/

# login.htmlを生成
cat > login.html << EOF
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ログイン</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 100px auto;
            padding: 20px;
            text-align: center;
            background-color: #f5f5f5;
        }
        .container {
            background-color: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 { color: #2c3e50; }
        .login-btn {
            display: inline-block;
            background-color: #3498db;
            color: white;
            padding: 15px 30px;
            text-decoration: none;
            border-radius: 5px;
            margin-top: 20px;
        }
        .login-btn:hover { background-color: #2980b9; }
    </style>
</head>
<body>
    <div class="container" id="login-container">
        <h1>ようこそ</h1>
        <p>コンテンツを閲覧するにはログインが必要です。</p>
        <a href="https://$COGNITO_DOMAIN.auth.us-east-1.amazoncognito.com/login?client_id=$CLIENT_ID&response_type=token&scope=openid+email+profile&redirect_uri=https://$CLOUDFRONT_DOMAIN/index.html" class="login-btn">ログイン</a>
    </div>

    <script>
        (function() {
            const hash = window.location.hash.substring(1);
            const params = new URLSearchParams(hash);
            const id_token = params.get('id_token');

            // URLにid_tokenが含まれている場合(後半でCognitoの設定変更後に有効になる)
            if (id_token) {
                document.getElementById('login-container').innerHTML = "<p>ログイン処理中...</p>";
                document.cookie = \`id_token=\${id_token}; path=/; max-age=86400; SameSite=Lax; Secure\`;
                window.location.href = '/index.html';
            }
        })();
    </script>
</body>
</html>
EOF

# S3に再アップロード
aws s3 cp login.html s3://$BUCKET_NAME/
2.5.2.CloudFrontの設定修正
# 現在のCloudFront設定を取得
ETAG=$(aws cloudfront get-distribution-config --id $DISTRIBUTION_ID --query 'ETag' --output text)
DIST_CONFIG=$(aws cloudfront get-distribution-config --id $DISTRIBUTION_ID --query 'DistributionConfig')

# DefaultRootObjectを 'login.html' に変更
UPDATED_CONFIG=$(echo $DIST_CONFIG | jq '.DefaultRootObject = "login.html"')

# CloudFrontを更新
aws cloudfront update-distribution --id $DISTRIBUTION_ID --distribution-config "$UPDATED_CONFIG" --if-match $ETAG

3.挙動確認

3.1.CloudFrontのURL取得

# 変数の確認
echo "CloudFront URL: https://$CLOUDFRONT_DOMAIN"

3.2.フロント画面

フロント画面

3.3.ログイン画面

ログイン画面

3.4.ログイン後の画面

ログイン後の画面

4.おわりに

4.1.得られた知見

  • Cognito に Lambdaをアタッチして検証などが可能
  • 上記呼出しの際、リソースベースポリシーを作成(Cognito → Lambdaの部分)
  • TTL設定(0秒)でキャッシュを無効化することで、認証状態の即座反映が可能

4.2.今後の課題

  • 後半のJWTトークン検証部分の構築
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?