AWSの部屋

AWS学習者向けのブログです

Cognito を使ったユーザ認証で S3 にアクセスしてみる

はじめに

アプリケーションの開発時、認証部分の開発は必須かと思います。しかし、認証まわりの開発は地味ですが意外と手間と時間がかかります。Cognito は認証基盤を短期間でアプリケーションに実装することができるサービスで認証部分の開発工数削減が見込めます。

今回のゴール

Cognito でユーザ認証を行い S3 にアクセスしてみようと思います。

使用するサービス

サービス 説明
Amazon Cognito Web アプリケーションやモバイルアプリケーションに対する安全な認証を提供するサービス
Amazon Simple Storage Service (Amazon S3) データを格納・管理できるオブジェクトストレージサービス

Amazon Cognito の主な 2 つのコンポーネント

Amazon Cognito にはユーザープールと ID プールというコンポーネントがあります。

ユーザープール

認証基盤を開発しなくてもウェブおよびモバイルアプリケーションからのサインアップ、サインインに使用することができます。ユーザーの ID やパスワードの認証情報をアプリ内部の「ユーザーディレクトリ」という領域に保存し、その情報を利用してアプリの「認証」を行います(ユーザープールはアクセスしてきたユーザーが誰であるかを確認します)。また、Cognito ユーザープールのみで認証することもできますが、SNS など外部の認証も使用することができます。

ID プール

AWS の他のサービスへのアクセス権をユーザーに付与する AWS 認証情報を提供します。例えば、Amazon S3バケットAmazon DynamoDB テーブルなどへのアクセスをユーザーに許可します。また、ID プールには認証プロバイダーを設定できます。認証プロバイダーには、SNS などの Web ID フェデレーションや Cognito ユーザープールが設定できます。

ユーザープールと ID プールの違い

「ユーザープール」は「認証(アクセスしてきたユーザーが誰か)」を処理しますが、「ID プール」は「認可(そのユーザーが利用できるサービスであるか)」を処理するという違いがあります。
また、「ユーザープール」の認証処理の対象は「アプリ」ですが、「ID プール」は「AWS のサービス」が対象となってるところも違いの一つです。

ユーザープールと ID プールをどのように活用すべきか

ユーザープールと ID プールの活用のポイントは、両方ともセットで使うことです(「認証(誰が)」と「認可(何のサービスを)」を組み合わせて利用することではじめて認証サービスとして機能するため)。

アーキテクチャ

S3 に対する読み取りだけ許可をするように Cognito で制御します。

実行環境

macOS 12.3.1
Python 3.9.6
Boto3 1.20.53

手順

  1. ユーザープールの作成
  2. ID プールの作成
  3. ユーザ登録
  4. S3 にアクセス

1. ユーザープールの作成

以下の設定でユーザープールを作成します。

項目
プロバイダーのタイプ Cognito ユーザープール
Cognito ユーザープールのサインインオプション E メール
パスワードポリシーモード Cognito のデフォルト
多要素認証 MFAなし
ユーザーアカウントの復旧 デフォルト
属性検証とユーザーアカウントの確認 デフォルト
属性変更の確認 デフォルト
必須の属性 email
属性変更の確認 デフォルト
E メール Cognito で E メールを送信
ユーザープール名 任意のユーザープール名
ホストされた認証ページ デフォルト
アプリケーションタイプ 秘密クライアント
ホストされた認証ページ デフォルト
アプリケーションクライアント名 任意のアプリケーションクライアント名
クライアントのシークレット クライアントのシークレットを生成しない
高度なアプリケーションクライアントの設定 デフォルト
属性の読み取りおよび書き込み許可 デフォルト

2. ID プールの作成

以下の設定で ID プールを作成します。

項目
ID プール名 任意のID プール名
認証されていない ID デフォルト
認証フローの設定 デフォルト
認証プロバイダー Cognito
ユーザープール ID 作成したユーザープールのユーザープール ID
アプリクライアント ID 作成したユーザープールのクライアント ID

ID プールの作成後にロールの作成を求められます。

ポリシーを以下のように設定します。

認証ユーザに付与するロール
"Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*",
        "cognito-identity:*",
        "s3:Get*",
         "s3:List*",
         "s3-object-lambda:Get*",
         "s3-object-lambda:List*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}
非認証ユーザに付与するロール
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]

3. ユーザ登録

ユーザープール内にユーザが未登録なため、ユーザの登録をします。

本来は認証画面を作りたいところなのですが、時間的に難しいので以下のコードでユーザ登録を行います。

import boto3

idp_client = boto3.client('cognito-idp')

aws_result = idp_client.admin_create_user(
    # ユーザープールID
    UserPoolId='xxxxx',
    Username='xxx@gmail.com', # なぜかUsernameにメールアドレスを求められる・・・
    UserAttributes=[
        {
            'Name': 'email',
            'Value': 'xxx@gmail.com'
        },
        {
            'Name': 'email_verified',
            'Value': 'true'
        }
    ],
    TemporaryPassword = 'xxx'
)

ユーザが作成されました。コードの詳細はこちらこちらでご確認ください。

4. S3 にアクセス

S3 にある以下のバケットのオブジェクトを取得します。

import boto3

# CognitoIdentityProviderクライアント(Using Amazon Cognito user pools API)
idp_client = boto3.client('cognito-idp')

# CognitoIdentityクライアント(Using Amazon Cognito Federated Identities)
identity_client = boto3.client('cognito-identity')

# 設定情報
account_id = 'xxx'
region = 'ap-northeast-1'
user_pool_id = 'xxx'
identity_pool_id = 'xxx'
client_id = 'xxx'
auth_flow = "ADMIN_NO_SRP_AUTH"
user_name = 'xxx'
password = 'xxx'
logins_key = f"cognito-idp.{region}.amazonaws.com/{user_pool_id}"

def admin_initiate_auth():
    response = idp_client.admin_initiate_auth(
                UserPoolId = user_pool_id,
                ClientId = client_id,
                AuthFlow = auth_flow,
                AuthParameters={
                    "USERNAME": user_name,
                    "PASSWORD": password,
                }
            )
    return response

def get_id(id_token):
    response = identity_client.get_id(
        AccountId = account_id,
        IdentityPoolId = identity_pool_id,
        Logins={
            logins_key: id_token
        }
    )
    return response

def get_credentials_for_identity(identity_id, id_token):
    response = identity_client.get_credentials_for_identity(
        IdentityId = identity_id,
        Logins = {
            logins_key: id_token
        }
    )
    return response

# ログイン
auth_response = admin_initiate_auth()

# IDトークンを取得
id_token = auth_response['AuthenticationResult']['IdToken']

# アイデンティティIDを取得
id_response = get_id(id_token)
identity_id = id_response['IdentityId']

# S3にアクセスするクレデンシャルを取得
credentials_response = get_credentials_for_identity(identity_id, id_token)
Credentials = credentials_response['Credentials']
access_key_id = Credentials['AccessKeyId']
secret_access_key = Credentials['SecretKey']
session_token = Credentials['SessionToken']

# S3にアクセス
s3_client = boto3.client('s3',
            region_name="ap-northeast-1",
            aws_access_key_id=access_key_id,
            aws_secret_access_key=secret_access_key,
            aws_session_token=session_token
        )

# バケットにあるアイテムを検索
object_list = s3_client.list_objects_v2(Bucket='cf-templates-lxrja6wfp65n-ap-northeast-1')
for obj in object_list['Contents']:
    print(obj['Key'])

以下のようにバケット内のアイテムを取得できました。

20221507Y9-network_and_server.yml
2022150G7w-network_and_server.yml
2022150HcQ-network_and_server.yml
2022150KyE-network_and_server.yml
2022150clO-network_and_server.yml
2022150mbX-network_and_server.yml
2022150nGG-network_and_server.yml
2022150yTO-network_and_server.yml

バケットにオブジェクトを PUT すると Access Denied されます(想定通りの動き)

s3_client.put_object(Body = 'IMG_0687.jpeg', Bucket='cf-templates-lxrja6wfp65n-ap-northeast-1', Key = 'test.jpeg')

さいごに

少し長くなってしまいましたが、Cognito を使って S3 にアクセスすることができました。ユーザープールや ID プールなどややっこしくて理解するのに時間がかかりましたが、いい勉強になりました。次は認証画面も作ってみようと思います。