Amazon CognitoとAzure AD B2Cを利用したシングルサインオン

:::note 
この記事は2024/11/25に作成しています。CognitoのUIが切り替わった時期で、従来の手順とも変わっているので、正確ではない記述が含まれている可能性もあります。
:::

# はじめに
弊社は Amazon Cognitoを利用しているSaaSとAzure AD B2Cを利用しているSaaSを開発しています。
この２つのサービスは独立しており、それぞれのサービスが認証機能やユーザー管理を持っています。
また、それぞれのサービスが独自のユーザー管理方式と外部のIdPを活用したシングルサインオン（SSO）の仕組みを持っています。この２つのサービスは、Google認証やAzure Entra ID認証を利用すればSSOはできていました。ですが、独自のユーザー管理方式でもSSOを行いたいという要件が出てきました。
この要件を実現する手段として、独自のユーザー管理をAmazon Cognitoで行い、それぞれのサービスのSSO（OIDC(Open ID Connect)）の仕組みを利用することで、要件を実現しました。この手順をご紹介します。

# ゴール
:::note
WebアプリがAmazon CognitoやAzure AD B2Cを使えることを前提としています。
Amazon CognitoやAzure AD B2Cの使い方については本記事では対象にしていません。
:::

「BEFORE」のサービス構成を「GOAL」のサービス構成になるようにします。
Identify Provider(Idp)：Amazon Cognito
Relying Party (RP) ：Amazon CognitoとAzure AD B2C
認証はOIDC(Open ID Connect)で行います。

BEFORE
```plantuml
@startuml
left to right direction

actor ユーザー as user

rectangle ServiceA {
    rectangle ServiceA_ログイン画面 as ServiceA_loginUI
    rectangle "Azure AD B2C"<<RP>> as ServiceA_AzureADB2C
    rectangle ServiceA_ユーザー管理 as ServiceA_loginLogic
    rectangle ユーザー情報<<DB>> as ServiceA_DB
}
user -- ServiceA_loginUI
ServiceA_loginUI -- ServiceA_loginLogic : 独自ユーザー管理の場合
ServiceA_loginUI -- ServiceA_AzureADB2C : SSOの場合
ServiceA_loginLogic -- ServiceA_DB

rectangle ServiceB {
    rectangle ServiceB_ログイン画面 as ServiceB_loginUI
    rectangle "Amazon Cognito"<<RP>> as ServiceB_AWSCognito_rp
    rectangle ServiceB_ユーザー管理 as ServiceB_loginLogic
}

rectangle IdPs {
    rectangle " Google \n Azure Entra ID \n etc..."<<Idp>> as etcIdp
    rectangle "Amazon Cognito"<<IdP>> as ServiceB_AWSCognito_idp
}
user -- ServiceB_loginUI
ServiceB_loginUI -- ServiceB_loginLogic : 独自ユーザー管理の場合
ServiceB_loginLogic -- ServiceB_AWSCognito_idp
ServiceB_loginUI -- ServiceB_AWSCognito_rp : SSOの場合

ServiceA_AzureADB2C --- etcIdp
ServiceB_AWSCognito_rp --- etcIdp

@enduml
```
GOAL
```plantuml
@startuml
left to right direction

actor ユーザー as user

rectangle ServiceA {
    rectangle ServiceA_ログイン画面as ServiceA_loginUI
    rectangle "Azure AD B2C"<<RP>> as ServiceA_AzureADB2C
    rectangle ServiceA_ユーザー管理 as ServiceA_loginLogic
    rectangle ユーザー情報<<DB>> as ServiceA_DB
}
user -- ServiceA_loginUI
ServiceA_loginUI -- ServiceA_loginLogic : 独自ユーザー管理の場合
ServiceA_loginUI -- ServiceA_AzureADB2C : SSOの場合
ServiceA_loginLogic -- ServiceA_DB

rectangle ServiceB {
    rectangle ServiceB_ログイン画面 as ServiceB_loginUI
    rectangle "Amazon Cognito"<<RP>> as ServiceB_AWSCognito_rp
    rectangle ServiceB_ユーザー管理 as ServiceB_loginLogic
}

rectangle IdPs {
    rectangle " Google \n Azure Entra ID \n etc..."<<Idp>> as etcIdp
    rectangle "Amazon Cognito"<<IdP>> as ServiceB_AWSCognito_idp #pink
}

user -- ServiceB_loginUI
ServiceB_loginUI -- ServiceB_loginLogic : 独自ユーザー管理の場合
ServiceB_loginLogic -- ServiceB_AWSCognito_idp
ServiceB_loginUI -- ServiceB_AWSCognito_rp : SSOの場合

ServiceA_AzureADB2C -[#red,thickness=3]-- ServiceB_AWSCognito_idp #text:red : 独自ユーザーSSOの場合
ServiceB_AWSCognito_rp -[#red,thickness=3]-- ServiceB_AWSCognito_idp #text:red : 独自ユーザーSSOの場合

ServiceA_AzureADB2C --- etcIdp
ServiceB_AWSCognito_rp --- etcIdp

@enduml
```

:::note
以降の説明では、わかりやすくするためシークレットの値も記載しています。
記事の投稿時には該当の環境は削除しています。
:::

# ユーザープール（Amazon Cognito）
以下のユーザープール(userpool-test)が存在しているとします。
※ユーザープールＩＤ：ap-northeast-1_VZWZPzQ7e
ユーザープールの作成方法については本記事では触れません。
なお、Eメールで認証を行っているとしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/741180a2-6ea0-90ce-87c7-471825bc45d1.png)

また、以下のアプリケーションクライアント（org-app）が作成されているとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/5c9931e9-d177-fa6f-e660-dd8ca5c20487.png)

後述の設定を行うことで、このアプリケーションクライアント（org-app）をRelying Party (RP) の役割にします。

# Amazon Cognitoの設定
ここからが本題です。Amazon CognitoでAmazon Cognitoと連携します。
ややこしいですが、CognitoからEntraIDをIdpとして利用するのと同じ考えです。EntraIDではなくCognitoで認証します。
作成したユーザープール(userpool-test)に対して以下の設定を行います。
・Idp用のアプリケーションクライアントの登録
・アイデンティティプロバイダーの登録

## Idp用のアプリケーションクライアントの登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/02491029-e9c3-3d88-066f-a27e5712c2dd.png)

上記のアプリケーションクライアントの画面からIdp用のアプリケーションクライアント（idp-app)を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/62bb52bb-ffc4-73a0-f57b-7e261fae4c71.png)

アプリケーションタイプは「従来のウェブアプリケーション」を選択してください。
「従来のウェブアプリケーション」を選択することで、クライアントシークレットが発行されたアプリケーションクライアントを作成できます。Idp用のアプリケーションクライアントではクライアントシークレットが必要です。クライアントシークレットは後から発行することはできません。

リターンＵＲＬ（許可されているコールバックＵＲＬのこと）には以下を設定します。
https\://ユーザープール名.auth.ap-northeast-1.amazoncognito.com/oauth2/idpresponse
今回の環境では以下になります。
```
https://userpool-test.auth.ap-northeast-1.amazoncognito.com/oauth2/idpresponse
```

このＵＲＬはCognitoを利用しているサービス向けのコールバックＵＲＬです。
Cognitoのエンドポイントについては以下を参考にしてください。
[Identity provider and relying party endpoints](https://docs.aws.amazon.com/cognito/latest/developerguide/federation-endpoints.html)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/6ceba5c5-867f-d716-9aac-8a843ff4cc43.png)

上記のようなアプリケーションクライアントを作成します。
アプリケーションクライアントの追加後に「ログインページ」の「編集」ボタンで編集します。以下の設定が行われていることを確認してください。
・ＩＤプロバイダー：Cognitoユーザープールディレクトリ
・OAuth付与タイプ：認証コード付与
・OpenID Connect のスコープ：openid

なお、「許可されているコールバックＵＲＬ」を追加しています。追加したＵＲＬはAzure AD B2Cを利用しているサービス向けのコールバックＵＲＬです。「許可されているコールバックＵＲＬ」の設定は重要です。設定が異なっているとログイン画面が表示されません。以下のＵＲＬを設定します。
https\://テナント名.b2clogin.com/テナント名.onmicrosoft.com/oauth2/authresp
テナント名は利用するAzure AD B2Cのドメイン名を確認してください。ドメイン名が「xxxxx.onmicrosoft.com」の場合、テナント名が「xxxxx」になります。設定するURLは以下になります。
```
https://xxxxx.b2clogin.com/xxxxx.onmicrosoft.com/oauth2/authresp
```

アプリケーションクライアントとしては以下の2つが存在することになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/fcda916f-debe-0300-9d14-3c9a5e67430d.png)

## アイデンティティプロバイダーの登録
「ソーシャルプロバイダーと外部プロバイダー」でアイデンティティプロバイダーとしてCognitoのユーザープールが使える設定を行います。
※いろいろな「プロバイダー」という言葉がでてきていますね。もう少し統一感があってもいいと思いますが・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/bf0a759b-29a4-43d3-e0ba-7f337d3955d0.png)

「アイデンティティプロバイダーを追加」ボタンを押下して、アイデンティティプロバイダーを登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/82eff252-766b-92a0-2b22-89da4d13c708.png)

アイデンティティプロバイダーとしては「OpenID Connect(OIDC)」を選択します。
以下の設定を行います。
プロバイダー名：任意の値
クライアントＩＤ：
Idp用のアプリケーションクライアント（idp-app）に設定されているクライアントＩＤ
クライアントシークレット：
Idp用のアプリケーションクライアント（idp-app）に設定されているクライアントシークレット

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/cc35b061-d9d4-755c-e9e9-e2fcaac2151e.png)

許可されたスコープ：openid
属性のリクエストメソッド：GET
セットアップ方法：発行者 URL を通じた自動入力
発行者ＵＲＬ：
https\://cognito-idp.ap-northeast-1.amazonaws.com/ユーザープールＩＤ
今回の環境では以下になります。
```
https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_VZWZPzQ7e
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/149a3dad-fb1f-7fe9-2b85-e875dfbe48b7.png)

上記のようにアイデンティティプロバイダーが登録されます。
以上で、Cognito側の設定は完了のはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/7da971f5-9b70-6e41-c344-212b614b3410.png)

Idp用のアプリケーションクライアントでログインページが表示できるか確認してみます。上記の画面で「ログインページを表示」をクリックします。
クリックするとCognitoが用意してくれたログイン画面が表示されるはずですが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/69615c22-e78e-4d2f-fe87-03148c07f644.png)

？？？
```
Login pages unavailable
Please contact an administrator.
```

なんでしょう？？？
ここで、はまりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/d2edd496-84f5-dac5-2ad5-4002d9e92c53.png)

いろいろと確認するとIdp用のアプリケーションクライアントがマネージドログインを使う設定になっているようですが、スタイルが割り当たっていないことが原因でした。マネージドログインの機能は2024/11のアップデートで追加されました。アップデート後に環境を構築したのは今回が初めてだったので、原因を突き止めるのに時間がかかりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/286d1233-91a0-474e-d6a8-70b3a81da3af.png)

idp用のアプリケーションクライアントにもスタイルを割り当てたところ、無事にログイン画面が表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/72e7f41f-fc05-4c03-32e5-6a206c33e022.png)

「マネージドログイン」は従来の「ホストされたUI」の機能よりもできることが多くなっており、今後使えそうです。いろいろと試してみたいと思っています。

# Azure AD B2Cの設定
さて、次はAzure AD B2CでAmazon Cognitoと連携します。
Azure AD B2Cをすでに使っている場合を想定しているので、Azure AD B2Cの開始方法については触れません。
新しく使う場合は以下の記事などを参考にしてください。
[チュートリアル:Azure Active Directory B2C テナントの作成](https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/tutorial-create-tenant)

Azure AD B2Cでは以下の機能を使用します。
・アプリケーションの登録
・OpenID Connectプロバイダーの登録
・ユーザーフローの登録

## アプリケーションの登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/956166a7-09c2-4eb9-17b0-8f8c780dd041.png)

まずは「アプリの登録」から新しいアプリケーションを登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/b7cf8e70-ed97-94a0-77d2-66ee5a6e1e36.png)

以下の設定を行います。
名前：任意の名前
サポートされているアカウントの種類：
任意の ID プロバイダーまたは組織ディレクトリ内のアカウント (ユーザーフローを使用したユーザーの認証用)
リダイレクトＵＲＩ：
Azure AD B2Cを使うサービスのログイン画面などのURI
※今回はテスト用に「Web」としてhttps\://www.amazon.co.jp/を設定しています。
　本来は適切なURIを設定する必要があります。

## OpenID Connectプロバイダーの登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/6e170dc1-bd76-9acc-0056-a62453ba7976.png)

「IDプロバイダー」画面で「新しいOpenID Connectプロバイダー」ボタンを押下して、接続先の情報を設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/83bb9f03-78b0-d874-1f67-04a854d7a4ce.png)

以下の値を設定します。

| 項目| 値 |
|:-|:-|
| 名前   |任意の名前   |
| メタデータ URL  | 利用するCognitoのメタデータ URL(*1)  |
| クライアント ID  | Idp用のアプリケーションクライアント(idp-app)のクライアントＩＤ  |
| クライアントシークレット  | Idp用のアプリケーションクライアント(idp-app)のクライアントシークレット  |
| スコープ  | openid  |
| 応答の種類  | code  |
| 応答モード  | form_post  |
| ドメインのヒント  |   |
| ユーザーID  | sub |
| 表示名  | name |
| 名  |   |
| 姓  |   |
| 電子メール  | email |

(*1)
CognitoのメタデータＵＲＬは以下になります。
https\://cognito-idp.リージョン.amazonaws.com/ユーザープールＩＤ/.well-known/openid-configuration
今回の環境では以下になります。
```
https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_VZWZPzQ7e/.well-known/openid-configuration
```
再び記載しますが、Cognitoのエンドポイントについては以下を参考にしてください。
[Identity provider and relying party endpoints](https://docs.aws.amazon.com/cognito/latest/developerguide/federation-endpoints.html)

## ユーザーフローの登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/fd6ae909-e19f-28c5-8764-d51ac70c3ce4.png)

最後にユーザーフローの登録を行います。
「ユーザーフロー」から「新しいユーザーフロー」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/8d822792-52dc-bb45-bf34-b345f30232eb.png)

ユーザーフロータイプは「サインアップとサインイン」を選択肢、バージョンは「推奨」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/ef9186e7-8226-9ac9-ba77-c010ca6b21f1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/38a71863-0279-35d8-187e-512284cc751d.png)

以下を設定します。
※テスト用の設定を記載しています。使う環境にあわせて設定してください。

名前：任意の名前
ローカルアカウント：None
カスタムＩＤプロバイダー：cognito　※「IDプロバイダー」で追加したIDプロバイダー名です。
方法の種類：メール
ＭＦＡの強制:オフ
条件付きアクセスポリシーを適用する：オフ
ユーザー属性とトークン要求：メールアドレスの「属性を収集する」をオン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/0d7d01f6-3a60-370f-b725-001e274882ff.png)

ユーザーフローを登録したら、登録されたユーザーフローを選択して、Cognitoに接続できるか確かめます。「ユーザーフローを実行します」のボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/681878fd-1148-d12b-c469-2b1590cdfdb7.png)

「アプリケーション」と「応答ＵＲＬ」が選択できるので、今回作成したアプリケーションおよび、アプリケーションに登録したリダイレクトＵＲＩを選択します。
「ユーザーフローを実行します」のボタンを押下すると、下記のようにCognitoで用意されているログイン画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/6b16ce51-7d4a-65ca-0990-92ec1bbcd420.png)


# 検証
Cognitoのユーザープールにユーザーを追加して、Azure AD B2Cのユーザーフローを試してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/e6f8554c-8a82-079e-7cd7-009543f00ab0.png)

自分が使えるメールアドレスをユーザーとして登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/7db96633-617c-8520-3a2f-134b85d3db89.png)

ユーザーフローを実行し、ログイン画面で登録したメールアドレスおよびパスワードを設定します。最初は仮パスワードの状態なので、検証コードがログインしようとするメールアドレスに送信されます。このあたりのフローも用意してくれるのでいいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623058/16648896-5b03-7df6-28e2-007d93e4e691.png)

ログインに成功すると応答ＵＲＬに設定されていたアマゾンのページに遷移します。ログアウト処理を実装していないので、再度ユーザーフローを実行した場合は、（サインインされている状態なので）ログイン画面は経由せずにアマゾンのページに遷移されます。

# まとめ
弊社では既存サービスのSaaS化を進めていますが、Amazon CognitoもしくはAzure AD B2Cを使っているサービスについては簡単にシングルサインオンが実現できそうです。Amazon CognitoやAzure AD B2Cを使うことで、いろいろなIdpに簡単に対応できるのはすばらしいと改めて感じました。