🧃

【Cognito】ユーザープール作ったはいいけどどうやって操作するんや【AWS Amplify / Boto3】

2024/05/27に公開

Cognito でユーザープールを作成したものの、その先のすべてが分からず詰まってしまったのでまとめました。
同じく悩まれている方の参考になれば幸いです。

そもそも Cognito / AWS Amplify / Boto3 ってなんだ

Cognito とは、 AWS が提供する、ユーザー情報を保管・管理するサービスです。
React からは AWS Amplify (以下、Amplify)を用いて、Python からは Boto3 を用いてアクセスすることができます。

Cognito の操作に関して、 Boto3 にできることは Amplify にもできます。そのため、簡易的な開発であればフロントエンド(Amplify)のみで操作を完結させることも視野に入るでしょう。
今回は付随するアクションを含めたい場合もあったため、バックエンド(Boto3)側からの操作も行いました。

この記事の前提

  • Cognito 用の IAM ユーザー及びロールを作成している。
  • Cognito でユーザープール及びテストユーザーを作成している。

この記事では、公式ドキュメントにあるようなエラー処理は全て省いています。
詳しくは公式ドキュメントか、上記リポジトリを参照ください。

必要なライブラリをインストール

まずはライブラリを入れておきましょう。

Amplify をインストール

React に Amplify の最新版(v6)をインストールします。

$ npm i aws-amplify

次に、以下を記述し、フロントエンドから Cognito のユーザープールにアクセスできるようにしておきます。

src/main.tsx
import { Amplify } from "aws-amplify";
import { cognitoConstants } from "constants/auth";

Amplify.configure(cognitoConstants);

ReactDOM.createRoot(document.getElementById("root")!).render(
  ...
);
src/constants/auth.ts
import { ResourcesConfig } from "aws-amplify";

export const cognitoConstants: ResourcesConfig = {
  Auth: {
    Cognito: {
      userPoolClientId: import.meta.env.VITE_AWS_USER_POOL_CLIENT_ID,
      userPoolId: import.meta.env.VITE_AWS_USER_POOL_ID,
    },
  },
};

ユーザープールクライアント ID とユーザープール ID は、 Cognito から払い出された値を記述してください。

また、本記事では取り扱いませんが、ログイン機構に関してはとっても便利なライブラリもあります。

Boto3 をインストール

Python に Boto3 をインストールします。

$ pip install boto3

次に、Cognito 操作のための関数を作成しておきます。

import boto3

cognito = boto3.client(
  "cognito-idp",
  aws_access_key_id=os.environ["AWS_COGNITO_ACCESS_KEY"],
  aws_secret_access_key=os.environ["AWS_COGNITO_SECRET_ACCESS_KEY"],
  region_name=os.environ["AWS_PROJECT_REGION"]
)

引数の中身については下記を参照ください。

  • Boto3 は Cognito のみならず、 AWS の各種サービスを操作するためのライブラリです。その中で Cognito の操作を行うことを明示するため、第一引数に cognito-idp を指定します。
  • aws_access_key_idaws_secret_access_key では、 Cognito 操作用に作成した IAM ユーザーのアクセスキーとシークレットキーを指定します。
  • region_name では AWS のリージョンを指定します。

Boto3 では、 Amplify で用いた ユーザープールクライアント ID とユーザープール ID を用いないことに注意しましょう。同様に Amplify では、IAM ユーザーの各キーを入力することはありません。

バックエンドにユーザートークンを送信する準備

前述の通り、Cognito の操作は Amplify / Boto3 双方から行えます。但し Boto3 から行う場合は、予めバックエンドにユーザー情報を識別するトークンを渡しておく必要があります。
但し、後述の内容を含むため、一旦この章はスルーして 1 から読み進めていただいても OK です。

トークンってなにをするためにあるの?

トークンは、Cognito に登録されているユーザーを識別するためにあります。
例えばあなたが既にサービスにログインしており、フロントエンドでユーザー ID をグローバルに保管してあります。さらにあなたは自身のユーザー属性を確認するページにアクセスしようとします。
そのとき、フロントエンドからユーザー ID をそのままバックエンドに送信して、その ID に 紐づいているデータを取ってくる…ということは推奨されません。
理由は2つあります。

  1. ユーザー ID はあなた以外の人間も知っている可能性があるから(例:SNS アカウントなど)。
  2. フロントエンドからバックエンドに送信する値はいくらでも改竄のしようがあり、ユーザー ID を悪用し別人になりすましてアクセスしている可能性があるから。

つまり、バックエンドはユーザー ID を与えられたところで、それがあなた自身のものであると信頼することができないのです。
けれどもバックエンドは、与えられた情報をもとに、なにかしらのレスポンスを行わなければなりません。ユーザー ID よりも信頼できる別の方法が必要になります。

ここで用いられるのがトークンです。トークンはセッションごとに発行し直してくれる、ユニークかつ一時的な文字列です。このセッションが切れるまで、トークンはユーザーを識別する値として機能します。

ログインに成功すると、Cognito はトークンを一緒に返してくれます。
以下はAmplify からログインした例ですが、 Boto3 からログインしたとしても、同じようにトークンが返されるものと思います。

ユーザー属性を確認するページにアクセスするとき、バックエンドにはユーザー ID の代わりにこれを渡します。

トークンの正しさを判断するため、 発行元である Cognito に向けてバックエンドは問い合わせを行います。「こんなトークンがフロントから送られてきたけれど、お宅が発行したもので合ってる?であれば、紐づいているアカウントを教えて?」という具合です。
すると Cognito はその値をもとにユーザー情報を返却してくれます。バックエンドがそれをフロントエンドに渡すことで、初めて UI として描画ができるのです。

トークンの取得方法

1-1 で触れているように、トークンを取得するには Amplify の fetchAuthSession() を用います。
文字列としてのトークンを下記のように取得します。

import { fetchAuthSession } from "aws-amplify/auth";

const session = await fetchAuthSession();
const accessToken = session.tokens?.accessToken.toString();

これを context などに保管し、グローバルな値としてアプリケーションで用いると便利です。

フロントエンドからは、 headers の Authorization に Bearer トークンとして値を入れ、バックエンドに送信します。

await fetch(`${API_PATH}/example`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

フロントで送信した headers を、バックエンドからは request の中身を取り出す形で受け取ります。
送信する値は Bearer ${accessToken} と間に空白を入れているため、 spilt() でトークンそのものを切り出します。

@app.route("/example", methods=["GET"])
def example():
  original_access_token = app_todo.current_request.headers.get('Authorization', None)
  split_access_token = original_access_token.split()
  return split_access_token[1]

1. Amplify からサインイン、サインアップ、サインアウトする

これら3点に関しては特別バックエンドで機構を作る必要がないと感じたため、すべて Amplify から操作しました。
公式ドキュメントは以下です。

https://docs.amplify.aws/gen1/react/build-a-backend/auth/enable-sign-up/

1-1. サインインし、ユーザー情報を確認する

まずは AWS コンソールで作成したテストユーザーでサインインしてみます。

import { signIn } from "aws-amplify/auth";

await signIn({
  username,
  password,
});

サインインできました。

サインインしたユーザーに関しての情報を取得する API は二種類あります。
ユーザー ID 、トークンなど、セッションに関する情報は fetchAuthSession() を用いて取得します。Cognito に登録してあるユーザー属性などは fetchUserAttributes() を用いて取得します。

import {
  AuthSession,
  FetchUserAttributesOutput,
  fetchAuthSession,
  fetchUserAttributes,
} from "aws-amplify/auth";

const session: AuthSession = await fetchAuthSession();

if (session.tokens) {
  const userAttributes: FetchUserAttributesOutput = await fetchUserAttributes();
}

型を確認すると、上記二つの中身が以下のようにセットされてるとわかります。

/**
 * `session` には以下のセッション情報が含まれています。
 */
export interface AuthSession {
  tokens?: AuthTokens;
  credentials?: AWSCredentials;
  identityId?: string;
  userSub?: string;
}

/**
 * `userAttributes` には以下のユーザー属性情報が含まれています。
 */
export type AuthUserAttributes<UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey> = {
  [Attribute in UserAttributeKey]?: string;
};
export type AuthUserAttributeKey = AuthStandardAttributeKey | AuthAnyAttribute;
export type AuthStandardAttributeKey = 'address' | 'birthdate' | 'email_verified' | 'family_name' | 'gender' | 'given_name' | 'locale' | 'middle_name' | 'name' | 'nickname' | 'phone_number_verified' | 'picture' | 'preferred_username' | 'profile' | 'sub' | 'updated_at' | 'website' | 'zoneinfo' | AuthVerifiableAttributeKey;
export type AuthVerifiableAttributeKey = 'email' | 'phone_number';

前述の通り、 fetchAuthSession() にはセッションに関する情報が、 fetchUserAttributes() にはユーザーの属性情報が含まれていることがわかります。Boto3 を操作する際は、 fetchAuthSession() 内の tokens前述の通りバックエンドに送信する必要があります。Amplify からの場合は不要です。

1-2. サインアウトする

先程サインインしたアカウントで、サインアウトしてみます。

import { signOut } from "aws-amplify/auth";

await signOut();

サインアウトできました。
僕はこれを action ではなく通常の関数内で行ったので、序でに useRevalidator()revalidator.revalidate() して描画を更新しました。

1-3. サインアップする

画面上でサインアップを行ってみましょう。
まず、 emailpassword を Cognito に送信します。 username は必須ですが、ID を別途用意させることがないのであれば、 email をそのまま送ってしまってよいと思います。

import { signUp } from "aws-amplify/auth";

await signUp({
  username: email,
  password,
  options: {
    userAttributes: {
      email,
    },
  },
});

すると、登録したいメールアドレスに認証コードが送られてきます。
これを以下のように送信します。

import { confirmSignUp } from "aws-amplify/auth";

await confirmSignUp({
  username, // 先ほどと同じもの(email)をもう一度送る
  confirmationCode, // 届いた認証コードを送る
});

これでメールアドレス認証が完了します。
認証をせずともアカウント登録はできますが、 Cognito 上で 確認済み が付与されない状態となってしまいます。

2. ユーザー属性を更新する

2-1. Amplify からユーザー属性を更新する(例:メールアドレス)

ユーザー属性を Amplify から更新するとき、 updateUserAttribute() を用います。
例えばメールアドレスを更新する場合、attributeKey にユーザー属性のキーとして email を、 value に新しく設定したいメールアドレスを与えます。

import { updateUserAttribute } from "aws-amplify/auth";

await updateUserAttribute({
  userAttribute: {
    attributeKey: "email",
    value: email,
  },
});

すると認証コードが新しいメールアドレスに送られてくるので、これを confirmUserAttribute() に送信します。

import { confirmUserAttribute } from "aws-amplify/auth";

await confirmUserAttribute({
  userAttributeKey: "email",
  confirmationCode: code,
});

2-2. Boto3 からユーザー属性を更新する(例:プロフィールアイコン)

Boto3 から Cognito を操作します。今回はプロフィールアイコンの更新を行います。
一連のフローは以下の通りです。

  1. フロントエンドから画像とトークンを受け取り、画像は S3 に保存
  2. Cognito ユーザー属性の picture を呼び出し、中身があれば(過去に一度でもプロフィールアイコンアイコンを登録していれば)その値を一時的に保管
  3. S3 に保存した filename を用いて picture を更新
  4. 3 に値があれば、該当するファイルを S3 から削除

今回は Cognito に関わる 23 を取り扱います。
S3 まわりについては こっちの記事に少しだけ書いています。

まずは、ユーザー属性を取得する処理を行います。操作内容はコメントに記述しています。

get_user_attribute(access_token: str, attribute: str):
  # 1. 先ほど作った関数である `cognito` より、 `get_user` を呼び出す
  user = cognito.get_user(AccessToken=access_token)
  # 2. user の中身から `UserAttributes` オブジェクトを取り出す
  user_attributes = user["UserAttributes"]
  # 3. 取得したい attribute をフィルタする
  result_object = next(filter(lambda x: x["Name"] == attribute, user_attributes), None)
  # 4. result_object の中身から `Value` オブジェクトを取り出す
  result = result_object["Value"]
  return result

# 手順2)Cognito ユーザー属性の `picture` を呼び出し
# 中身があれば(過去に一度でもプロフィールアイコンアイコンを登録していれば)
# その値を一時的に保管する
old_picture = get_user_attribute(access_token="ACCESS_TOKEN", attribute="picture")

これでユーザー属性(今回は picture )を取得することができました。

さらに、ユーザー属性を更新する処理を行います。

update_user_attribute(access_token: str, attribute: str, value: str):
  cognito.update_user_attributes(
    AccessToken=access_token,
      UserAttributes=[
        {
          "Name": attribute,
          "Value": value
        },
      ],
    )
    return True

update_user_attribute(
  access_token="ACCESS_TOKEN",
  attribute="picture",
  value="S3_FILE_NAME"
)

こちらはとても簡単ですね。関数を呼び出し、値を送信するだけで完了します。
Cognito 用に作成している IAM ロールに権限(get_userupdate_user_attributes)を忘れず追加しておきましょう。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cognito-idp:GetUser",
        "cognito-idp:UpdateUserAttributes"
      ],
      "Resource": [
        "XXX"
      ]
    }
  ]
}

3. パスワードを更新する

3-1. Amplify からパスワードを更新する

パスワードを Amplify から更新するときは、 updateUserAttribute() ではなく updatePassword() を用います。
oldPasswordnewPassword として古いパスワードと新しいパスワードを送信します。
特に認証コード等が届くことはなく、そのまま更新することができます。

import { updatePassword } from "aws-amplify/auth";

await updatePassword({
  oldPassword,
  newPassword,
});

3-2. Boto3 からパスワードを更新する

Boto3 では change_password を用いるようです。詳しくは公式ドキュメントを参照ください。
IAM ロールの更新も忘れず行なっておきましょう。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp/client/change_password.html

4. ユーザーを削除する

4-1. Amplify からユーザーを削除する

リポジトリでは Boto3 から操作した内容ですが、 Amplify からユーザーを削除する場合は以下で大丈夫なようです。

import { deleteUser } from 'aws-amplify/auth';

await deleteUser();

詳しくは公式ドキュメントを参照ください。

https://docs.amplify.aws/gen1/react/build-a-backend/auth/delete-user-account/

4-1. Boto3 からユーザーを削除する

上記のようにAmplify から操作することもできますが、今回は 2-2-1 で S3 にアップロードした画像の削除も行いたいと考えており、処理が少し複雑になってしまうことから、リポジトリでは Boto3 で操作しています。
Cognito の内容ではないので詳細は省きますが、 2-2-1 同様に old_picture として S3 のファイル名を保管しておき、値がある且つユーザー削除に成功した場合にこちらも削除します。

他にも、ユーザーに紐づいている DB のデータ等があれば、ここで一緒に消してしまうのがよいと思います。

delete_user(access_token: str):
  cognito.delete_user(
    AccessToken=access_token,
  )
  return True

delete_user(access_token="ACCESS_TOKEN")

Cognito の操作のみであれば一瞬で終わってしまいますね。
こちらも IAM ロールに delete_user の権限を与えて終了です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cognito-idp:GetUser",
        "cognito-idp:UpdateUserAttributes",
        "cognito-idp:DeleteUser"
      ],
      "Resource": [
        "XXX"
      ]
    }
  ]
}

Amplify と Boto3 どちらも同様なのですが、よくある退会処理として挙がりそうな、パスワードを入力させたりといった手順がまったく要らない仕様なので、そこに少し不安が残ります…。

リポジトリ

https://github.com/poetrainy/todoapp-python

本記事では一部の箇所でコードを例示しましたが、全てこのリポジトリから抜粋したものです。全コードをご覧になりたい方は上記を参照ください。

終わりに

今回は Amplify / Boto3 で操作する Cognito の主要機能をご説明しました。
フロントエンドとバックエンド、双方からアクセスを行ったので個人的にとても勉強になりました。

Discussion