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?

【教えて下さい】CognitoでS3にOAuth認証してみた

Posted at

はじめに

  • Amazon Cognitoを使ってS3の静的ウェブサイトに認証をかけてみました
  • CloudFrontのLambda@Edgeを組み合わせ、Google OAuthを利用したログインを実現しています
  • ですが、私自身もまだ完全には理解できていない部分が多いため、実際の設定手順や試行錯誤のプロセスを共有し、有識者の方からのフィードバックをいただけると嬉しいです

やったことの概要

  1. S3バケットの作成
    • 静的コンテンツを格納するためのS3バケットを作成しました
  2. CloudFrontのオリジンとして設定
    • S3バケットをCloudFrontのオリジンに設定し、OAC (Origin Access Control) を使用してセキュアにアクセスできるようにしました
  3. GCPでOAuth同意画面を作成
    • Cognitoと連携するため、Google Cloud Platform (GCP) 上でOAuth同意画面を作成しました
  4. Amazon Cognitoユーザープールの作成
    • Cognitoのユーザープールを作成し、IDプロバイダーとしてGoogleを設定しました
  5. Lambda関数の作成
    • 認証フローやログアウト処理を行うためのLambda関数をPythonで実装しました
  6. CloudFrontのLambda@Edgeに設定
    • CloudFrontのビューワーリクエストイベントに対し、Lambda@Edgeとして設定しました

S3にバケットを作る

  • バケットを作成
  • パブリックアクセスは許可しない
    S3_バケットを作成___S3___ap-northeast-1_20241114_170113.png
    S3_バケットを作成___S3___ap-northeast-1_20241114_170158.png
  • 認証が必要なページのhtmlをバケットにおく
  • パブリックアクセスをブロックしているのでブラウザで開けない
    oguro-052864411610_s3_ap-northeast-1_amazonaws_com_index_html_20241114_172227.png

CloudFrontでディストリビューションを作る

  • OACを作る
    Cursor_と_Amazon_CloudFront_20241114_171111.png

  • ディストリビューションを作成

  • キャッシュのあたりがよくわかってない・・
    Amazon_CloudFront_20241114_171157.png
    Cursor_と_Amazon_CloudFront_20241114_171321.png
    Cursor_と_Amazon_CloudFront_20241114_171335.png

  • ディストリビューションを作ると画面上部に出てくる

  • コピーして、バケットポリシーに貼り付ける
    Amazon_CloudFront_20241114_171416.png
    バケットポリシーを編集_-_S3_バケット_oguro-052864411610___S3___ap-northeast-1_20241114_171523.png

  • これで、CloudFrontを経由すればブラウザで開くようになる
    認証成功_20241114_172058.png

GCPでOAuth 同意画面とクライアントを作る

  • 新しいプロジェクト
    新しいプロジェクト_–gravityga_jp–_Google_Cloud_コンソール_20241114_174436.png
    Cursor_と_API_とサービス_–oguro-cognito–_Google_Cloud_コンソール_20241114_174558.png
    アプリ登録の編集_–API_とサービス–oguro-cognito–_Google_Cloud_コンソール_20241114_175027.png

User Type

  • 内部
    • GCPプロジェクトが組織に属している場合
      • Google Workspaceのユーザーだけが通過できる
    • GCPプロジェクトが組織に属していない場合
      • 誰も通過できない
    • 外部
    • Googleアカウントなら誰でも通過できる

認証情報を作成する

  • OAuth クライアントID
    Cursor_と_認証情報_–API_とサービス–oguro-cognito–_Google_Cloud_コンソール_20241114_175227.png
    Cursor_と_OAuth_クライアント_ID_の作成_–API_とサービス–oguro-cognito–_Google_Cloud_コンソール_20241114_175313.png
    認証情報_–API_とサービス–oguro-cognito–_Google_Cloud_コンソール_20241114_175348.png
  • 承認済みのリダイレクト URIは後で設定が必要になる
  • クライアントIDとシークレットをメモっておく

Cognitoでユーザープールを作る

  • ユーザープールを作成
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_175746.png
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_175801.png
    • Eメール
    • Google
  • セキュリティ要件を設定
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_175846.png
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_175857.png
    • MFAなし
  • サインアップエクスペリエンスを設定
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_180006.png
    Cursor_と_ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_180038.png
    • 自己登録を有効化 のチェックを外す
  • メッセージ配信を設定
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_180144.png
    • CognitoでEメールを送信
  • フェデレーテッドアイデンティティプロバイダーを接続
    Cursor_と_ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_180438.png
    • さっきGCPで作ったクライアントの情報を貼り付ける
    • 許可されたスコープ
      • openid email profile
  • アプリケーションを統合
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_181343.png
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_181417.png
    ユーザープールを作成___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241114_181502.png

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にビヘイビアを作る

  • /token /logout の2つ
    Cursor_と_Amazon_CloudFront_20241116_124022.png

CloudFrontのLambda@Edgeに登録する

  • まずはロールを追加する
    sam-cognito-python-google-ValidateCognitoJWTFuncti-0n9oz1fxHCva___関数___Lambda_20241116_125140.png
    sam-cognito-python-google-ValidateCognitoJWTFuncti-0n9oz1fxHCva___関数___Lambda_20241116_125103.png
    sam-cognito-python-google-ValidateCognitoJWTFuncti-0n9oz1fxHCva___関数___Lambda_20241116_125219.png
    sam-cognito-python-google-ValidateCognitoJWTFunctio-cWoasxnRR7r2___IAM___Global_20241116_125339.png
  • CloudFrontのLambda@Edgeに設定される
    Cursor_と_Amazon_CloudFront_20241116_193750.png

3つのLambdaをそれぞれのビヘイビアに登録する

  • デフォルト (*)
    • ValidateCognitoJWTFunc
  • /logout
    • CognitoLogoutHandlerFunc
  • /token
    • CognitoTokenRedirectFunc

Cognitoの設定を修正

  • ホストされたUIを編集
    ホストされた_UI_を編集___アプリケーションクライアント__oguro-google-client___oguro-cognito-pool___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241116_195002.png
    ホストされた_UI_を編集___アプリケーションクライアント__oguro-google-client___oguro-cognito-pool___ユーザープール___Amazon_Cognito___ap-northeast-1___Cognito___ap-northeast-1_20241116_195017.png
    • コールバックURLを修正
    • サインアウトURLを修正
    • IDプロバイダーからCognitoユーザープールを削除

GCPの設定を変更

動作確認

自己フィードバック

  • Cognitoの設定に関して苦戦しました
    • 「パブリッククライアント」と「秘密クライアント」の違いがいまいち理解できていません
    • クライアントシークレットを生成しましたが、これが正しい対応だったのか不明です
  • Lambdaのコードについて
    • シークレットを直接コード内に書いてしまいました(反省しています)
    • JWTを扱うパッケージが大きすぎたので、チェックが簡易版です・・
    • Pythonで実装しましたが、認証周りではNode.jsの方が適しているようなので、そちらで作り直そうと考えています

さいごに

  • Cognitoは予想以上に難しかったです・・
  • 一応動くものは作れましたが、自分でも改善点が多いと感じています
  • 有識者の皆さまからアドバイスやフィードバックをいただけると非常に助かります!
  • 作成したものは現在、セキュリティの観点から停止済みです
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?