【AWS Amplify × Cognito】サインインオプション変更で大苦戦! Authを作り直すプロセスまとめ

# こんな人に読んでほしい！
* サインインオプションを変更したい人
* `amplify add auth` でサインインオプションを仮決めしたけど、後で変更する方法とその工数が気になる人
* amplifyでauthを作り直したい人

# なぜAuthを作り直すのか？

<strong>サインインオプションを変えたかっただけなのに...</strong>

なぜAuthの設定を作り直すことにしたのか？
それは、メールアドレスでサインインしたかったからです。
既存の認証設定ではサインインオプションが「ユーザー名」になっていたため、メールアドレスでのサインインができませんでした。
![スクリーンショット 2024-12-11 17.57.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/db60c217-c4fb-b070-358d-828091bef3ff.png)

ところが、Amazon Cognito ではサインインオプションを変更することができないと[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pool-updating.html#cognito-user-pool-updating-fixed-settings)よりわかりました。

>ユーザープールを作成した後に、この設定を変更することはできません。以下の設定を変更する場合は、新しいユーザープールまたはアプリクライアントを作成する必要があります。

<strong>サインインオプションの変更はできない...だと！？</strong>

やりたかったことは2つ
* 商用環境でも既に使用しているユーザープールをそのまま維持し、既存のユーザーと設定を保持したい
* 新しいユーザープールを作成し、それをAmplifyに紐付けたい

いくつかの問題に直面しましたが、無事にやりたいことを実現することができました。
今回はその経験をもとに、困ったことや解決方法を共有します。
# 予想外だった課題

Amplifyで認証設定（auth）を新しいものに切り替える際に直面した困難と、そこで気づいたことを3つ紹介します。

### `amplify add auth`は既存のauthがあると実行できない

既存のauthが紐づけられている場合、以下のように`amplify add auth`コマンドが効きません。
```terminal
$ amplify add auth
⚠️ Auth has already been added to this project. To update run amplify update auth.
```
amplifyが複数個のauthの紐付けに対応していたら楽だったんですが、今紐づいているauthを`amplify remove auth`する必要があります。


### `amplify remove auth`は、「リソースを消すか？」の質問にyesでなければ実行されない

:::note info
`amplify add auth`で作成したauthを切り離す場合を前提としています。
`amplify import auth`で外部のauthを取り込んだ場合、この問題は発生しません。
:::

既存authをamplifyから切り離そうとすると
```terminal
$ amplify remove auth
? Are you sure you want to delete the resource? This action deletes all files related to this resource from the backend directory. 
```
「ソースを削除してもよろしいですか？」と確認されます。
既存のアプリがすでに商用で使われている場合、現行のAuthをそのまま削除するわけにはいきません。しかし、ここで「No」と答えると`amplify remove auth`の処理は中断されてしまいます

そのため、<strong>「リソースを消すか」の質問でyesと答えつつ、リソースそのものが消されない工夫をする必要があります</strong>

### `amplify remove auth`はauthとの依存関係が残っていると実行できない

`amplify remove auth`は、少しでもauthに依存しているリソースが残っていると以下のようにエラーが発生します。

例1: analyticsがauthに依存しているためエラー
```terminal
$ amplify remove auth
🛑 Auth cannot be removed because the analytics category depends on it
Resolution: Run `amplify remove analytics` first, then retry removing auth
Learn more at: https://docs.amplify.aws/cli/project/troubleshooting/
```

例2: userPoolGroupsがauth(userPool)に依存しているためエラー
```terminal
$ amplify remove auth
🛑 Resource cannot be removed because it has a dependency on another resource
Dependency: Cognito-UserPool-Groups - userPoolGroups. Remove the dependency first.
```
例3: Lambdaがauthに依存しているためエラー
```terminal
$ amplify remove auth
🛑 Resource cannot be removed because it has a dependency on another resource
Dependency: Lambda - hogefunctionName. Remove the dependency first.
```
例4: AppSyncがauthに依存しているためエラー
```terminal
$ amplify remove auth
🛑 Resource cannot be removed because it has a dependency on another resource
Dependency: AppSync - viteproject. Remove the dependency first.
```

そのため、**`amplify remove auth` を実行する前に、auth に依存しているリソースを全て切り離す必要があります**

# Amplifyで新authに切り替える
ここでは、実際に課題を解決するために行った具体的な手順について説明します。
## リソース削除を阻止する
### 削除ポリシーについて
* 削除ポリシーとは
CloudFormationにおいてリソースを削除する際に、そのリソースが意図せず削除されないように保護するための設定のこと

Amplifyでは、リソース（例えば、auth）を作成する際に、CloudFormationを使用します。通常、リソースを削除する際は、以下の手順を踏みます：
1. `amplify remove ~`コマンドで、削除したいリソースの設定を削除する
2. `amplify push`を実行して、リソースの削除をCloudFormationに適用させる

ただし、Cloudformationでリソースを削除する前に**削除ポリシーを設定しておくことで、リソース本体が削除されるのを防ぐ**ことができます

### authへの削除ポリシーの付与

削除ポリシーを設定する基本的な方法は、CloudFormationテンプレートファイルに `"DeletionPolicy": "Retain"` を追加することです。（例↓）

```cloudformation-template.json
"Resources":{
   "UserPool": {
     "Type": "AWS::Cognito::UserPool",
     "DeletionPolicy": "Retain", //　←⭐️ここ
   }
}
```
しかし、Amplifyでauthフォルダ内の`cloudformation-template.json`に同じ方法で記述しましたが、うまくいきませんでした。

理由は、`amplify/backend/auth` フォルダ内を見ると、`build` フォルダの中に`cloudformation-template.json `が存在するためです。

![スクリーンショット 2024-12-07 0.45.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/5c138212-1f6d-9ef5-f01b-300e7a71040e.png)

`build` フォルダはAmplifyによって自動生成・上書きされるため、手動で変更しても変更内容は適用されません。

そのため、Cognitoリソースを上書きするために次のコマンドを使用します
```terminal
$ amplify override auth
```

![スクリーンショット 2024-12-15 0.01.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/db1e330d-8dc6-98c9-c8cd-0304d2ee5666.png)

* 結果
build外にoverride.tsファイルが追加されました。

ここに⭐️のコードを追加します

```override.ts
import { AmplifyAuthCognitoStackTemplate, AmplifyProjectInfo } from '@aws-amplify/cli-extensibility-helper';

export function override(resources: AmplifyAuthCognitoStackTemplate, 
amplifyProjectInfo: AmplifyProjectInfo) {
    resources.userPool.addOverride('DeletionPolicy', 'Retain') // ⭐️1行追加
}
```

* **UserPool以外にも残しておきたいリソースがある場合の削除ポリシーの書き方**
    1. `build/hoge-cloudformation-template.json`を開き、対象リソースを確認して残すものを選ぶ
    2. 削除ポリシー設定
userPoolClientの削除を阻止する場合は、
`resources.userPoolClient.addOverride('DeletionPolicy', 'Retain');`のように設定する

削除ポリシーの設定を付与したら
`amplify push`を実行して、変更をCloudFormationに適用させます。

### 適用されたことの確認

削除ポリシーが適用されたかを確認するためには、`build`フォルダ内にある`cloudformation-template.json`を確認するのが簡単です。以下のように削除ポリシーが追加されていれば、設定が正しく適用されています。
```auth/authhoge/build/hogehoge-cloudformation-template.json
   "UserPool": {
     "Type": "AWS::Cognito::UserPool",
     "Properties": {},
     "DeletionPolicy": "Retain" ←⭐️適用された
   },
```
cloufgormation-templateの中身は、AWSマネジメントコンソールのCloudFormationからも確認できます。

これで、削除ポリシーがリソースに追加され、`amplify remove auth`を実行してもリソースが削除されないようになりました。

:::note warn
開発環境と商用環境がある場合、`amplify remove auth` を実行する前に商用環境にも削除ポリシーを適用させてください。
理由は削除ポリシー後の`amplify remove auth` の実行で、削除ポリシーが適用されたファイルごと削除されてしまうからです。
:::


## authとの依存関係をなくす
### Lambda
```terminal
$ amplify update function
? Select the Lambda function you want to update hogefunctionName
? Which setting do you want to update? Resource access permissions
```
![スクリーンショット 2024-12-07 12.19.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/cbff2a46-da26-454c-315d-0c89a04d8eb1.png)

### AppSync
この依存を断ち切るためには`amplify update api`コマンドで、以下でCognito User Pool以外を選ぶ必要があります。

![スクリーンショット 2024-12-07 14.46.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/91c4c224-5034-7e6b-5b7c-3f3c194523ef.png)

しかし、このコマンドは schema.graphql に `@auth` が記述されている場合、正常に実行できません。

* そのため、まず `@auth` の部分を取り除きます

```diff:amplify/backent/api/hoge/schema.graphql
type Todo @model
- @auth(rules: [
- { allow: public, operations: [read] },
- { allow: owner, ownerField: "owner", operations: [create, update, delete] } 
- ]) {
+ {
id: ID!
name: String!
owner: String
}
```
* その後`amplify update api`でGraphQLの依存先を、CognitoからAPI Keyに変更します。

```terminal
$ amplify update api
? Select from one of the below mentioned services: GraphQL
? Select a setting to edit Authorization modes
? Choose the default authorization type for the API API key
✔ Enter a description for the API key: · 
✔ After how many days from now the API key should expire (1-365): · 7
? Configure additional auth types? No
```
この設定変更により、Cognitoの依存関係が解除され、API Keyを利用した認証に変更されます

※ この変更はGraphQLをauthから引き離すための暫定的な措置です。authの作り直しと紐付けが終われば再度Authorization modesを「Cognito User Pool」に戻します。

### Analytics

`amplify update analytics`コマンドで、Pinpoint（Analytics）とauthの依存関係を断ち切れるか試してみましたが、残念ながら以下のように、設定変更の機能が提供されていませんでした
```
amplify update analytics
? Select from one of the below mentioned services: Amazon Pinpoint
🛑 Update functionality not available for this service
```
そのため、removeしました。
```terminal
$ amplify remove analyics
```
過去の分析データを消したくない場合は、先に削除ポリシーを付与する必要がありますので注意してください。


### その他
function, api, analytics, userPoolGroups以外の部分でauthを参照している箇所があるとエラーになることがあります。
自分の場合はAPI GatewayでCognitoユーザープールをオーソライザーとして使用しており、APIのoverride.tsの部分でuserpoolIdを参照していたためエラーになりました
```override.ts
'Fn::GetAtt': ['authresourcename', 'Outputs.UserPoolId'],
```
auth周り参照部分はコメントアウトしておきます

## `amplify remove auth`の実行
```terminal
$ amplify remove auth

? Are you sure you want to delete the resource? This action deletes all files related to this resource fr
om the backend directory. Yes
✅ Successfully removed resource
```
![スクリーンショット 2024-12-07 15.25.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/f5a39a08-5529-cb45-2a4a-aac4af37e929.png)

無事消えました。
やったこれでauthを追加することができます。


## 新しいauthの紐付け
### `amplify import auth`で既存のauthを紐付ける
既存のCognito User Poolを新しいAmplifyプロジェクトに紐付ける場合、`amplify import auth`コマンドを使用します。このコマンドを実行することで、既存の認証設定を新しいAmplifyプロジェクトにインポートできます。
```terminal
$ amplify import auth
Using service: Cognito, provided by: awscloudformation
? What type of auth resource do you want to import? … 
❯ Cognito User Pool and Identity Pool
  Cognito User Pool only
? Select the User Pool you want to import: … 
hogehoge11111111_userpool_11111111-dev (ap-northeast-1_iiiiiiiii)
hogehoge22222222_userpool_22222222-dev (ap-northeast-1_ooooooooo)
...userpoolがあるだけ表示されます。 ⭐️1つ選ぶ!
✔ Select a Web client to import: · hogehogeWebClient
✔ Select a Native client to import: hogehogeNativeClient
```

### `amplify add auth`で新規を作成・紐づける
`amplify add auth`コマンドを使って、新規の認証リソースを作成することもできます。この方法では、対話形式で様々な認証オプションを選択することができます。

色々選べますので、一例を載せます
```terminal
$ amplify add auth

 Do you want to use the default authentication and security configuration? (Use arrow keys)
❯ Default configuration 
  Default configuration with Social Provider (Federation) 
  Manual configuration 
  I want to learn more. 


 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? (Use arrow keys)
  Username 
❯ Email 
  Phone Number 
  Email or Phone Number 
  I want to learn more. 

Do you want to configure advanced settings? No, I am done.
✅ Successfully added auth resource viteproject11111111 locally
```

## 新しいauthに対して、依存先を向ける

以下のリソースが新しいAuth設定を参照するように向け直します：

- Lambda関数
- AppSync（GraphQL API）
- Analytics（Pinpoint）
- その他のauthをGetAttで参照しているリソース（例: override.ts）

上記の[authへの依存をなくす](#authとの依存関係をなくす)の逆の操作になるため、ここでは詳細を省略します。
ざっくりというと、`amplify update`コマンドを使用するか、override.tsなどでauthを参照している設定ファイルを編集するなどの操作を行います

## `amplify push`で新しいAuth設定を適用
お疲れ様でした。
`amplify push`します！

これでAmplifyで新authが適用され、サインアップ時には、新ユーザプールが使用されます！


# Cognitoユーザーの移行
:::note info
新しいユーザープールを作成するだけでは、旧ユーザープールに存在していたユーザー情報が新しいプールには含まれていません。そのため、既存のユーザーはサインインできないため、ユーザー移行を行う必要があります
:::

ユーザー移行の方法として、主に2つの方法があります。

1. ユーザー移行の Lambda トリガーを使用したユーザーのインポート
2. CSV ファイルからユーザープールにユーザーをインポートする

この記事では、 **CSV ファイルからユーザープールにユーザーをインポートする方法** のみを取り上げます。

また、これらの方法だけでは対応できない認証情報の移行について、特に<strong>サードパーティの認証情報の移行</strong>についても説明します。

## CSV ファイルからユーザープールにユーザーをインポートする

※ CognitoをCSV ファイル化する方法については記事内で取り上げません。

* **できること**
    * 標準ユーザ属性の移行

* **できないこと**
    * パスワードの移行
    * カスタムユーザー属性や、フェデレーションユーザー（例えば、GoogleやFacebook、LINEなどでサインインするユーザー）を既存のCognitoユーザープロファイルにリンクすること
    * 元のsubを引き継ぐ

**実際にやってみた**
  * 準備物
      * 移行するために必要な準備物は、ユーザー情報が入ったCSVファイルです。以下はその例です：
```test.csv
cognito:username,name,given_name,family_name,middle_name,nickname,preferred_username,profile,picture,website,email,email_verified,gender,birthdate,zoneinfo,locale,phone_number,phone_number_verified,address,updated_at,cognito:mfa_enabled
johndoe@example.com,,John,Doe,,,,,,,johndoe@example.com,TRUE,,02/01/1985,,,+12345550100,TRUE,123 Any Street,,FALSE
janeroe@example.com,,Jane,Roe,,,,,,,janeroe@example.com,TRUE,,01/01/1985,,,+12345550199,TRUE,100 Main Street,,FALSE
```
:::note info
csvの中身は[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-using-import-tool.html#cognito-user-pools-using-import-tool-csv-header)に従っていますが、そのまま使用すると次のようなエラーが発生しました
`[FAILED] Line Number 2 - Username should be an email.`
おそらく、サインインオプションが「email」を想定したサンプルではないため、エラーが発生したと思われます。そのため、cognito:usernameの部分を適切に変更しました
:::

- 次に、Cognitoマネジメントコンソールを使ってユーザーをインポートします
    - 「ユーザー」タブを選択し、左側のメニューから「インポートジョブを作成」を選び、CSVファイルをアップロードします

![スクリーンショット 2024-12-08 23.57.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/604b45da-726c-2c0b-b867-4b4e6b144a1d.png)

しばらく待つと、ユーザーが新しいユーザープールに作成されていることが確認できました。これで簡単に移行が完了です！

:::note warn
Cognitoではパスワードのインポートはサポートされていません。移行後、ユーザーは最初にサインインした際にパスワードを変更する必要があります。

画像にもある通り、移行後のユーザーは「パスワードのリセットが必要」な状態になります。つまり移行後すぐに[signIn](https://docs.amplify.aws/gen1/react/build-a-backend/auth/auth-migration-guide/#authsignin)メソッドでログインすることはできません。
[resetPassword](https://docs.amplify.aws/gen1/react/build-a-backend/auth/auth-migration-guide/#authsignin)  メソッドを使用して、ユーザーにパスワードのリセットを促す必要があります。
:::


## サードパーティの認証情報の移行

※ 次のような場合に必要です
- 移行元のUser Pool で、Google、Facebook、LINE などの外部認証サービスを利用しているユーザーがいる場合
- 既存の外部認証サービスを使用したサインインを新しいUser Poolでも継続させたい場合

Cognitoでは、サードパーティの認証情報（LINEやFacebookのユーザーIDなど）は、ユーザープールの `identities`として管理されていますが、この部分は、「CSVファイルからのインポート」では移行できません。


（例）LINEログインを利用して Cognito にサインインできるユーザーの情報
![スクリーンショット 2024-12-11 17.44.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/673cc864-83e8-47e9-7702-916c1d307182.png)

![スクリーンショット 2024-12-11 17.46.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/a90107a2-d552-beb5-c1e8-4c16c070c760.png)



そのため、サードパーティ認証情報を既存のCognitoユーザーにリンクさせるためには、[AdminLinkProviderForUser](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-identity-federation-consolidate-users.html) メソッドを使用する必要があります。

以下は、Cognito ユーザーに LINE の認証情報をリンクさせる Python のコード例です

```py:index.py
cognito_client = boto3.client('cognito-idp')
destination_user = {
        'ProviderName': 'Cognito',
        'ProviderAttributeValue': cognito_username # サードパーティ認証情報を追加したいCognitoユーザのusername
    }
source_user = {
        'ProviderName': 'LINE', # サードパーティ名
        'ProviderAttributeName': 'Cognito_Subject',
        'ProviderAttributeValue': line_user_id # サードパーティでのユーザID
    }
try:
    response = cognito_client.admin_link_provider_for_user(
            UserPoolId=USERPOOL_ID,
            DestinationUser=destination_user,
            SourceUser=source_user
    )
except:
    # 省略
```

# 認証メソッドの注意点と変更点
Amplify を使用したフロントエンド側での認証処理についての内容です。

## usernameとしてemailを渡して認証メソッドを使う
`signIn`, `signUp`, `confirmSignUp`, `resetPassword`など、さまざまな認証メソッドで`username`として`email`を使用する必要があります。

これに関する具体的なコード紹介します。

<strong>signInメソッド</strong>
```ts
import { signIn } from 'aws-amplify/auth';

// ↓ ① ❌ エラー発生
await signIn({ email, password });

// ↓ ② ⭕️ 使用可能
await signIn({ username: email, password });

// ↓ ③ ⭕️ 使用可能
const handleSignIn = (username: string, password: string) => {
  signIn({ username, password });
};
handleSignIn(email, password); // 引数に email を渡す
```
* signInに限らず、**ほかの認証メソッドでもusernameとしてemailを渡す形で機能します**

ただし、例外もあります。
サインインオプションに username と email の両方を設定した場合、サインアップで以下のエラーが発生することがあります。
![スクリーンショット 2024-12-11 16.22.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/de700315-c8d3-0702-2d83-d6bbed7134db.png)

`Error: Username cannot be of email format, since user pool is configured for email alias.`
この問題の詳細な対処法は本記事では触れませんが、注意が必要です。

## `username:email`としても、usernameがemailの値で登録されるわけではない

signUp メソッドでの動作についてです

<strong>signUpメソッド</strong>
サインインオプションが `email` のみに設定されている場合、signUp メソッドで `username: email` を使っても、ユーザー名（`username`）として `email` がそのまま登録されるわけではありません。

```ts
// サインインオプション:emailのみ
 await signUp({
   username: email,
   password,
   options:{
       userAttributes: {
           email,
         },
         autoSignIn: {
           enabled: true,
         },
   }
 });
```
* **結果**
`username = email` で登録されると思っている方もいるかもしれませんが、実際には `username` として uuidが自動的に生成されます。

![スクリーンショット 2024-12-08 22.07.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/c183d56e-08d7-85a2-6dce-ed37e095a31f.png)

# 最後に
多くの試行錯誤を経て、「auth(ユーザープール）の再構築と切り替え」と「ユーザ移行」を無事に行うことができました。記事の目次を見てもわかるように、サインインオプションを変更したいだけだったのですが思った以上に大変な作業でした。

そのため、これからサインインオプションを決定しようとしている方は、ぜひ慎重に決めていただきたいと思います。また、変更せざるを得ない状況の方には、少しでも参考になれば幸いです。
読んでいただき、ありがとうございました！