はじめに
- Amazon Cognitoを使ってS3の静的ウェブサイトに認証をかけてみました
- CloudFrontのLambda@Edgeを組み合わせ、Google OAuthを利用したログインを実現しています
- ですが、私自身もまだ完全には理解できていない部分が多いため、実際の設定手順や試行錯誤のプロセスを共有し、有識者の方からのフィードバックをいただけると嬉しいです
やったことの概要
- S3バケットの作成
- 静的コンテンツを格納するためのS3バケットを作成しました
- CloudFrontのオリジンとして設定
- S3バケットをCloudFrontのオリジンに設定し、OAC (Origin Access Control) を使用してセキュアにアクセスできるようにしました
- GCPでOAuth同意画面を作成
- Cognitoと連携するため、Google Cloud Platform (GCP) 上でOAuth同意画面を作成しました
- Amazon Cognitoユーザープールの作成
- Cognitoのユーザープールを作成し、IDプロバイダーとしてGoogleを設定しました
- Lambda関数の作成
- 認証フローやログアウト処理を行うためのLambda関数をPythonで実装しました
- CloudFrontのLambda@Edgeに設定
- CloudFrontのビューワーリクエストイベントに対し、Lambda@Edgeとして設定しました
S3にバケットを作る
CloudFrontでディストリビューションを作る
-
ディストリビューションを作成
-
ディストリビューションを作ると画面上部に出てくる
GCPでOAuth 同意画面とクライアントを作る
User Type
-
内部
- GCPプロジェクトが組織に属している場合
- Google Workspaceのユーザーだけが通過できる
- GCPプロジェクトが組織に属していない場合
- 誰も通過できない
外部
- Googleアカウントなら誰でも通過できる
- GCPプロジェクトが組織に属している場合
認証情報を作成する
Cognitoでユーザープールを作る
- ユーザープールを作成
- Eメール
- セキュリティ要件を設定
- MFAなし
- サインアップエクスペリエンスを設定
- 自己登録を有効化 のチェックを外す
- メッセージ配信を設定
- CognitoでEメールを送信
- フェデレーテッドアイデンティティプロバイダーを接続
- さっきGCPで作ったクライアントの情報を貼り付ける
- 許可されたスコープ
openid email profile
- アプリケーションを統合
- Cognito ドメインを使用する
- クライアントのシークレットは生成する
- リダイレクトURI
Lambdaを作る
- SAMで作った
- us-east-1にデプロイする
cognito_jwt_validator.py
import json
import base64
# ======== 設定 ========
COGNITO_POOL_ID = "ap-northeast-1_yeeHL6sOj"
COGNITO_CLIENT_ID = "5a1aiip7crdbaosrq0rnlnop91"
COGNITO_HOST_UI_DOMAIN = "oguro-google-01"
CLOUDFRONT_DOMAIN = "dj4ve2s8yqfir"
COGNITO_ISSUER = f"https://cognito-idp.ap-northeast-1.amazonaws.com/{COGNITO_POOL_ID}"
REDIRECT_URL = (
f"https://{COGNITO_HOST_UI_DOMAIN}.auth.ap-northeast-1.amazoncognito.com/login"
f"?client_id={COGNITO_CLIENT_ID}&response_type=code"
f"&scope=openid&redirect_uri=https://{CLOUDFRONT_DOMAIN}.cloudfront.net/token"
)
# JWTトークンの簡易検証
def validate_jwt_simplified(token):
try:
# トークンをヘッダー、ペイロード、署名に分割
parts = token.split('.')
if len(parts) != 3:
print("トークンの形式が不正です。")
return False
# ペイロード部分をデコード
payload_encoded = parts[1]
payload_decoded = base64.urlsafe_b64decode(payload_encoded + '=' * (-len(payload_encoded) % 4))
payload = json.loads(payload_decoded)
# 最低限のフィールド検証
if payload.get("iss") != COGNITO_ISSUER:
print("発行者が一致しません。")
return False
if payload.get("aud") != COGNITO_CLIENT_ID:
print("オーディエンスが一致しません。")
return False
print("トークンは有効です。")
return True
except Exception as e:
print(f"トークン検証中にエラーが発生しました: {e}")
return False
# Cookieを解析して辞書形式に変換
def parse_cookies(headers):
parsed_cookie = {}
if 'cookie' in headers:
for cookie in headers['cookie']:
for item in cookie['value'].split(';'):
parts = item.split('=')
if len(parts) == 2:
parsed_cookie[parts[0].strip()] = parts[1].strip()
return parsed_cookie
# Cookieから指定されたトークンを取得
def get_token_from_cookies(headers, token_name):
cookies = parse_cookies(headers)
return cookies.get(token_name)
# Lambdaハンドラー
def lambda_handler(event, context):
print(event)
request = event['Records'][0]['cf']['request']
# クッキーからIDトークンを取得
token = get_token_from_cookies(request['headers'], 'id_token')
print(f"取得したトークン: {token}")
# トークンが無い場合はリダイレクト
if not token:
return {
'status': '302',
'statusDescription': 'Found',
'headers': {
'location': [{'key': 'Location', 'value': REDIRECT_URL}]
}
}
# トークン検証
try:
if validate_jwt_simplified(token):
print("トークン検証成功")
else:
raise ValueError("トークンの検証に失敗しました")
except Exception as e:
print(f"認証エラー: {e}")
return {
'status': '302',
'statusDescription': 'Found',
'headers': {
'location': [{'key': 'Location', 'value': REDIRECT_URL}]
}
}
# 認証成功の場合、リクエストを通す
return request
cognito_token_redirect.py
import requests
import json
import urllib.parse
import base64
# 設定
COGNITO_CLIENT_ID = "5a1aiip7crdbaosrq0rnlnop91"
COGNITO_CLIENT_SECRET = "2kfjic08vg45ug3dso7uctsv9nmp4*************"
COGNITO_HOST_UI_DOMAIN = "oguro-google-01"
CLOUDFRONT_DOMAIN = "dj4ve2s8yqfir"
REDIRECT_URI = f"https://{CLOUDFRONT_DOMAIN}.cloudfront.net/token"
REDIRECT_AFTER_AUTH = f"https://{CLOUDFRONT_DOMAIN}.cloudfront.net/index.html"
# 認証情報をBase64エンコードで生成
def get_authorization():
auth_str = f"{COGNITO_CLIENT_ID}:{COGNITO_CLIENT_SECRET}"
return base64.b64encode(auth_str.encode("utf-8")).decode("utf-8")
# Authorization Codeを使用してトークンを取得
def get_tokens_from_code(code):
authorization = get_authorization()
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {authorization}"
}
data = {
"grant_type": "authorization_code",
"client_id": COGNITO_CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"code": code
}
token_url = f"https://{COGNITO_HOST_UI_DOMAIN}.auth.ap-northeast-1.amazoncognito.com/oauth2/token"
response = requests.post(token_url, headers=headers, data=data)
if response.status_code == 200:
tokens = response.json()
return tokens.get("access_token"), tokens.get("id_token")
else:
raise Exception(f"Failed to obtain tokens: {response.status_code} - {response.text}")
# Lambdaハンドラー
def lambda_handler(event, context):
# CloudFrontリクエストの取得
request = event['Records'][0]['cf']['request']
# クエリ文字列の解析
params = urllib.parse.parse_qs(request['querystring'])
code = params.get('code', [None])[0] # 'code' パラメータの取得
if not code:
return {
'statusCode': 400,
'body': json.dumps("Code parameter is missing in the URL.")
}
try:
# トークン取得
access_token, id_token = get_tokens_from_code(code)
# クッキー設定
cookie_value = f"id_token={id_token}; Secure; HttpOnly; SameSite=None; Path=/"
return {
'status': '302',
'statusDescription': 'Found',
'headers': {
'location': [{
'key': 'Location',
'value': REDIRECT_AFTER_AUTH
}],
'set-cookie': [{
'key': 'Set-Cookie',
'value': cookie_value
}],
'cache-control': [{
'key': 'Cache-Control',
'value': 'no-cache, no-store, must-revalidate'
}],
'pragma': [{
'key': 'Pragma',
'value': 'no-cache'
}],
'expires': [{
'key': 'Expires',
'value': '0'
}]
}
}
except Exception as e:
return {
'statusCode': 500,
'body': json.dumps(f"Error occurred: {str(e)}")
}
cognito_logout_handler.py
import json
# 設定
COGNITO_CLIENT_ID = "5a1aiip7crdbaosrq0rnlnop91"
COGNITO_HOST_UI_DOMAIN = "oguro-google-01"
CLOUDFRONT_DOMAIN = "dj4ve2s8yqfir"
LOGOUT_REDIRECT_URI = f"https://{CLOUDFRONT_DOMAIN}.cloudfront.net/index.html"
# CognitoのログアウトURLを構築
def build_cognito_logout_url():
return (
f"https://{COGNITO_HOST_UI_DOMAIN}.auth.ap-northeast-1.amazoncognito.com/logout"
f"?client_id={COGNITO_CLIENT_ID}&logout_uri={LOGOUT_REDIRECT_URI}"
)
# Lambdaハンドラー
def lambda_handler(event, context):
# CognitoのログアウトURL
cognito_logout_url = build_cognito_logout_url()
# クッキー削除のためのSet-Cookieヘッダー
set_cookie_headers = [
{
'key': 'Set-Cookie',
'value': 'id_token=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=None'
},
{
'key': 'Set-Cookie',
'value': 'access_token=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=None'
},
{
'key': 'Set-Cookie',
'value': 'refresh_token=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=None'
}
]
# レスポンスを構築
response = {
'status': '302', # リダイレクト
'statusDescription': 'Found',
'headers': {
'location': [{
'key': 'Location',
'value': cognito_logout_url # CognitoのログアウトURLにリダイレクト
}],
'set-cookie': set_cookie_headers,
'cache-control': [{
'key': 'Cache-Control',
'value': 'no-cache, no-store, must-revalidate'
}],
'pragma': [{
'key': 'Pragma',
'value': 'no-cache'
}],
'expires': [{
'key': 'Expires',
'value': '0'
}]
}
}
return response
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
Serverless Application Model Template for Cognito authentication and logout
with Python-based Lambda functions.
Globals:
Function:
Timeout: 3
MemorySize: 128
Resources:
ValidateCognitoJWTFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: cognito_jwt_validator.lambda_handler
Runtime: python3.12
Architectures:
- x86_64
Description: Validates JWT tokens issued by Cognito.
CognitoTokenRedirectFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: cognito_token_redirect.lambda_handler
Runtime: python3.12
Architectures:
- x86_64
Description: Handles redirect from Cognito and processes access tokens.
CognitoLogoutHandlerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: cognito_logout_handler.lambda_handler
Runtime: python3.12
Architectures:
- x86_64
Description: Logs out users from Cognito and clears authentication cookies.
Outputs:
ValidateCognitoJWTFunction:
Description: ARN of the ValidateCognitoJWTFunction
Value: !GetAtt ValidateCognitoJWTFunction.Arn
CognitoTokenRedirectFunction:
Description: ARN of the CognitoTokenRedirectFunction
Value: !GetAtt CognitoTokenRedirectFunction.Arn
CognitoLogoutHandlerFunction:
Description: ARN of the CognitoLogoutHandlerFunction
Value: !GetAtt CognitoLogoutHandlerFunction.Arn
CloudFrontにビヘイビアを作る
CloudFrontのLambda@Edgeに登録する
3つのLambdaをそれぞれのビヘイビアに登録する
- デフォルト (*)
- ValidateCognitoJWTFunc
- /logout
- CognitoLogoutHandlerFunc
- /token
- CognitoTokenRedirectFunc
Cognitoの設定を修正
GCPの設定を変更
動作確認
-
https://dj4ve2s8yqfir.cloudfront.net/index.html
- 組織外のGoogleアカウントは入れない
自己フィードバック
- Cognitoの設定に関して苦戦しました
- 「パブリッククライアント」と「秘密クライアント」の違いがいまいち理解できていません
- クライアントシークレットを生成しましたが、これが正しい対応だったのか不明です
- Lambdaのコードについて
- シークレットを直接コード内に書いてしまいました(反省しています)
- JWTを扱うパッケージが大きすぎたので、チェックが簡易版です・・
- Pythonで実装しましたが、認証周りではNode.jsの方が適しているようなので、そちらで作り直そうと考えています
さいごに
- Cognitoは予想以上に難しかったです・・
- 一応動くものは作れましたが、自分でも改善点が多いと感じています
- 有識者の皆さまからアドバイスやフィードバックをいただけると非常に助かります!
- 作成したものは現在、セキュリティの観点から停止済みです