Cognitoでメールアドレス認証ユーザとGoogle認証ユーザを統合する方法

# 目次
- [はじめに](#はじめに)
- [この記事で扱うこと](#この記事で扱うこと)
- [この記事で扱わないこと](#この記事で扱わないこと)
- [前提条件](#前提条件)
- [既存ユーザとGoogle認証ユーザを統合する方法](#既存ユーザとgoogle認証ユーザを統合する方法)
    - [全体の流れをざっくり説明](#全体の流れをざっくり説明)
    - [Lambdaの実装手順](#lambdaの実装手順)
    - [Cognitoの設定手順](#cognitoの設定手順)
- [動作確認](#動作確認)

# はじめに

Cognitoにて異なる認証方式を用いてサインアップした同一ユーザを、メールアドレスでマッピングする方法を記載します。

Cognitoではメールアドレスとパスワードを用いた認証以外に、GoogleやFacebookのアカウントを用いた認証も可能となっております。これは便利ではありますが、以下のような疑問点が出てくるのではないでしょうか。

##### 疑問点:既にメールアドレスとパスワードを用いてサインアップを行なっているユーザAが存在したとして、ユーザAがGoogleを使用してログインした場合に、ユーザ情報は別になるのか。


こちらについての結論は以下になります。
##### 結論:Cognitoでは認証方法が違う限り、メールアドレスが同じでも別々のユーザとして扱う。

これでは同じユーザが2つ以上のアカウントを持ってしまうことに繋がるため、今回はLambdaを使って、異なる認証方式を用いて認証を行なった同一メールアドレスを持つアカウントを統合する方法を記載します。

# この記事で扱うこと

- サインアップ前のLambdaトリガーを使用したユーザの統合方法
    - 既にEmailで認証済みのユーザがGoogle認証を行なった際に、同一ユーザとしてログイン出来るようにする方法

# この記事で扱わないこと

- Cognitoユーザプールの作成方法
- CognitoユーザプールへのGoogle認証の追加方法
- Lambdaの詳細説明
- 既にGoogleで認証済みのユーザがEmail認証を行なった際に、同一ユーザとしてログイン出来るようにする方法(重要)

# 前提条件

- Cognitoユーザプールを作成していること
    - 参考記事: https://qiita.com/Naoki1126/items/51fc6467157f4e6ee01e
- CognitoユーザプールにてEmailでの認証とGoogleの認証を可能にしていること
    - 参考記事: https://qiita.com/Naoki1126/items/2dbc8ac4eb07310adc0f
  
 
# 既存ユーザとGoogle認証ユーザを統合する方法

※記事の紹介でも触れましたが、今回の内容は「既にEmailで認証済みのユーザがGoogle認証を行なった際に、同一ユーザとしてログイン出来るようにする方法」であり、その逆の方法は記載しません。

## 全体の流れをざっくり説明

イメージを掴んでいただくために処理フローを記載します。
SNS認証をした際のCognito内のフローは通常以下のようになります。
※厳密には認可コードのやりとりなどが発生するため、もっと複雑な処理になりますがここでは一旦省略します。
![スクリーンショット 2023-05-05 15.53.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/71158807-6851-7c21-69b7-51d5626e0450.png)



そして今回はこのようなフローを実現させることを目的とします。
![スクリーンショット 2023-05-05 16.05.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/5d8f3f48-f963-f4c2-c1b0-fa9f00e82197.png)

Lambdaにて黄色く塗りつぶしている箇所を実装し、サインアップ前のLambdaトリガーにて発火させることで、このフローを実現します。

サインアップ前のLambdaトリガーの詳細はこちらから
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html


## Lambdaの実装手順

### 1.Lambdaを検索、「関数の作成」をクリック
![Lambda_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/ad309eb2-4e47-3ac5-b072-933dab4f682d.png)

### 2.関数の作成

今回は一から作成を選択し、言語はPython3.10、他は一旦全てデフォルトのままにして作成します。

![Lambda_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/e1d8ac06-22d5-106a-e79d-7cede9621954.png)


### 3.関数の中身を記載し、「デプロイ」を実行
![Lambda3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/eeb34bb7-df24-96b4-f37e-ea2fec098512.png)

コードは以下の通りです。

```cognitoIDMergeFunc.py
import json
import boto3

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

#対象のユーザープールIDを記載
user_pool_id = 'your_user_pool_id'

def lambda_handler(event, context):
    # ユーザープールに追加しているidp一覧
    idp_list = ['Google','Facebook']

    # SNS認証を使った場合、idpの名前の一部がuserNameから取得出来る(userName = google_xxxxyyyyy)
    # idpの名前が入っていなければSNS認証ユーザではないため、以降の処理を実施しない
    user_name = event['userName'].split('_')
    user_idp = ''
    for idp in idp_list:
        if idp.lower() == user_name[0]:
            user_idp = idp
    if user_idp == '':
        return event
    
    # ユーザープール内に同一メールアドレスを持つユーザがいないか検索をかける
    # list_usersに渡す検索文を作成する
    email_address = event['request']['userAttributes']['email']
    filter_str = 'email = ' + '"' + email_address + '"'

    # list_usersでユーザープール内のユーザを参照する
    # Filterとしてemailを指定する
    same_user = ''
    same_user = cognito_client.list_users(
            UserPoolId = user_pool_id,
            Filter = filter_str
    )
    
    # もし同一メールアドレスを使用したユーザがいなければ、統合処理は行わずユーザの新規作成を行う
    if same_user == '':
        return event
    
    # 同一メールアドレスを使用しているユーザがいれば、AdminLinkProviderForUserを使用してユーザ情報を統合する
    # Googleの場合ProviderAttributeValueは、「google_XXXXXXYYYYYY」の「XXXXXXYYYYYY」となる
    user_id = user_name[1]
    response = cognito_client.admin_link_provider_for_user(
        UserPoolId=user_pool_id,
        DestinationUser={
            'ProviderName': 'Cognito',
            'ProviderAttributeValue': same_user['Users'][0]['Username']
        },
        SourceUser={
            'ProviderName': user_idp,
            'ProviderAttributeName': 'Cognito_Subject',
            'ProviderAttributeValue': user_id
        }
    )
    
    return event

```

#### ListUsersについて

APIドキュメント
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListUsers.html

boto3ドキュメント
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp/client/list_users.html

#### AdminLinkProviderForUsersについて
APIドキュメント
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminLinkProviderForUser.html

boto3のドキュメント
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp/client/admin_link_provider_for_user.html

### 4.Lambdaのロールの設定変更

デフォルトの設定だとAdminLinkProviderForUsersやListUsersが実行できないため、実行ロールに関数の実行権限を与える必要があります。

![Lambda4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/8b52cc7b-9736-03d1-83b8-f93fae2825ac.png)

### 5. 設定画面にて「インラインポリシーを作成」を選択

![Lambda5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/4ead4517-b3f7-4218-82b9-be89533e7dfc.png)

### 6.関数の許可設定を行う

- サービスは「Cognito User Pools」
- アクションとして「ListUsers」と「AdminLinkProviderForUsers」を選択
![Lambda6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/eefccc97-eb00-fceb-9a54-7c55f5818323.png)

### 7.ポリシー名を設定し、「ポリシーの作成」を選択
![Lambda7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/af0fd17b-c021-d565-6aaa-d9e4f22f3ddd.png)

### 8.ポリシーを確認

許可ポリシー欄に作成したポリシーがあることを確認し完了です。
![Lambda8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/b8573580-b4f2-da12-268d-94b8a6329bb1.png)


## Cognitoの設定手順

### 1.ユーザープールの設定画面から「Lambdaトリガーを追加」を選択
![cognito_lambda1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/30ebebde-783f-6251-5384-2ed3e28e8057.png)

### 2. サインアップ前トリガーにて、先ほど作成したLambda関数を割り当てる

関数を割り当てて保存したら完了です。

![cognito_lambda2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/34afce80-37e0-36c2-d506-acd800d75545.png)


# 動作確認

## Lambdaを使用しないデフォルト状態

まずデフォルトの状態がどうなるかを事前にお見せします。
こちらの画像の通り、同一メールアドレスを持つユーザだとしても、ユーザプールでは別々のユーザとして扱われます。
![chack_1_standard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/2a257586-0b9c-7247-4362-f78f7e971ddf.png)

ユーザの詳細はこちらになります。
![chack_1_cognito.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/5a4949d3-1940-9664-0489-419df822325e.png)
![check1_google.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/e19f3549-7a4f-65b6-ea2e-a62f0191d4ec.png)


## Lambda関数使用後

こちらがLambda作成後です。
ユーザデータが一つであることが確認できます。
![check2_index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/3cf98eb0-c989-d82e-8de6-d68bd0732c40.png)

Lambdaにより、Email認証ユーザの「Identities」内にGoogleから取得した情報が保存されました。
これにてユーザの統合が完了です。
![check_2_user_sub.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/9102b39d-290f-d8d6-dd86-852625cf7c7f.png)

ログインも両方のアカウントで完了すること、ID Token内のSubjectがemailログイン時とGoogleログイン時で同じになることが確認できました。
![スクリーンショット 2023-05-05 18.17.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/5c561ca9-64f0-a1e6-79cd-0b7e0a8bf60d.png)

# まとめ
以上がCognitoでEmail認証ユーザとGoogle認証ユーザのアカウントを統合する方法でした。
AWSやPythonに明るいわけではないためベストプラクティスでない部分も多々あると思いますが、誰かのお役に立てれば幸いです。