AWS Amplify Gen 2 で外部IdPによるサインインを試してみた
こんにちは、なおにしです。
AWS Amplify Gen 2 の認証機能で外部IdPとしてFacebookアカウントとGoogleアカウントによるサインインを使用する機会がありましたのでご紹介します。
はじめに
AWS Amplify Gen 2 (以降、Amplify)では各種バックエンド要素をTypeScriptで宣言的に定義可能ですが、その中の一つに認証・認可があります。
Amplifyで定義された認証・認可はAmazon Cognitoによって提供されます。今回は、以下のドキュメントにある外部IdPを使用したサインイン機能の適用を実際にやってみます。
ドキュメントでは以下4つの外部IdPによる適用方法が紹介されていますが、今回はFacebookとGoogleのみを対象とします。
- Facebook Login
- Google Sign-In
- Login with Amazon
- Sign in with Apple
なお、それぞれの外部IdPでは最終的に本番利用する際は適用するアプリケーションの審査をそれぞれの外部IdPのコンソール画面を操作して受ける必要があります。ですが今回は外部IdPを使用したサインインが可能であることの確認までに留まった内容となりますので、各外部IdPにおける審査については別途外部IdP側のドキュメント等でご確認ください。
やってみた
前提
適用する対象は以下のReact+Viteによるクイックスタートの内容をサンドボックスで構築済みの環境とします。
Meta for DevelopersとGoogle Cloudのアカウントも開設済みとします。
Google Cloudについてはプロジェクトも作成済みとします。
Meta for Developersでの準備
Meta for Developersにサインインし、[マイアプリ] - [アプリを作成]に進んで各種項目を入力していきます。
ユースケースは以下のとおり選択します。ドキュメントに記載のUIからだいぶ変わっているようです。
今回は開発モードでの検証なので以下のとおり選択します。
内容を確認して[ダッシュボードに移動]を押します。
Facebookアカウントのパスワードを求められたら入力して送信します。すると以下のようにアプリが作成されます。
[アプリの設定] - [ベーシック]から「アプリID」と「app secret」を控えておきます。
Google Cloudでの準備
設定対象のプロジェクトを開き、[APIとサービス] - [OAuth 同意画面]を選択します。
既存設定が存在しない場合は[ブランディング]の設定から行います。
各種項目を入力して[作成]します。
[クライアント]を選択し、[クライアントを作成]からアプリケーションの種類を[ウェブ アプリケーション]として進みます。
任意の名前を設定して作成します。この時点ではURIの設定は必要ありません。
「クライアント ID」と「クライアント シークレット」を控えておきます。
サンドボックス環境へのシークレットの設定
今回はサンドボックス環境での検証を想定しているので、以下のとおりコマンドからシークレットを登録しておきます。
$ npx ampx sandbox secret set FACEBOOK_CLIENT_ID
? Enter secret value (上記で確認したアプリIDを入力)
Successfully created version 1 of secret FACEBOOK_CLIENT_ID
$ npx ampx sandbox secret set FACEBOOK_CLIENT_SECRET
? Enter secret value (上記で確認したapp secretを入力)
Successfully created version 1 of secret FACEBOOK_CLIENT_SECRET
$ npx ampx sandbox secret set GOOGLE_CLIENT_ID
? Enter secret value (上記で確認したクライアント IDを入力)
Successfully created version 1 of secret GOOGLE_CLIENT_ID
$ npx ampx sandbox secret set GOOGLE_CLIENT_SECRET
? Enter secret value (上記で確認したクライアント シークレットを入力)
Successfully created version 1 of secret GOOGLE_CLIENT_SECRET
$ npx ampx sandbox secret list
- FACEBOOK_CLIENT_ID
- FACEBOOK_CLIENT_SECRET
- GOOGLE_CLIENT_ID
- GOOGLE_CLIENT_SECRET
なお、登録したシークレットはSystems Manager パラメータストアに保存されます。
コードの適用
ドキュメントに記載のコードを参考に、defineAuthについては以下のように編集しました。
import { defineAuth, secret } from '@aws-amplify/backend';
export const auth = defineAuth({
loginWith: {
email: true,
externalProviders: {
facebook: {
clientId: secret('FACEBOOK_CLIENT_ID'),
clientSecret: secret('FACEBOOK_CLIENT_SECRET'),
scopes: ['email', 'public_profile'],
attributeMapping: {
familyName: 'last_name',
givenName: 'first_name',
fullname: 'name',
}
},
google: {
clientId: secret('GOOGLE_CLIENT_ID'),
clientSecret: secret('GOOGLE_CLIENT_SECRET'),
scopes: ['email', 'profile'],
attributeMapping: {
familyName: 'family_name',
givenName: 'given_name',
fullname: 'name',
}
},
callbackUrls: [
'http://localhost:5173/',
'https://(amplifyapp.comドメインやカスタムドメインを必要に応じて指定)/'
],
logoutUrls: [
'http://localhost:5173/',
'https://(amplifyapp.comドメインやカスタムドメインを必要に応じて指定)/'
]
}
}
});
Attribute mappingによって外部IdPから取得できる情報をCognitoに連携する際のマッピングを定義しています。マッピング可能な値はドキュメントや以下のようにCognitoのマネジメントコンソール画面から確認可能です。
また、せっかくなので外部IdPから取得した情報をブラウザにも表示できるようにしてみます。以下の記事を参考に編集しました。
import { useEffect, useState } from "react";
import type { Schema } from "../amplify/data/resource";
import { generateClient } from "aws-amplify/data";
import { Loader, useAuthenticator } from '@aws-amplify/ui-react';
import { FetchUserAttributesOutput, fetchUserAttributes } from 'aws-amplify/auth';
const client = generateClient<Schema>();
function App() {
const { signOut } = useAuthenticator();
const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);
const [userAttributes, setUserAttributes] = useState<FetchUserAttributesOutput>();
useEffect(() => {
client.models.Todo.observeQuery().subscribe({
next: (data) => setTodos([...data.items]),
});
const fetchAttributes = async () => {
try {
const result = await fetchUserAttributes();
setUserAttributes(result);
} catch (error) {
console.error('ユーザー属性の取得に失敗しました:', error);
}
};
fetchAttributes();
}, []);
function createTodo() {
client.models.Todo.create({ content: window.prompt("Todo content") });
}
function deleteTodo(id: string) {
client.models.Todo.delete({ id })
}
if (!userAttributes) {
return <Loader size="large" />;
}
return (
<main>
<h1>{userAttributes.family_name}'s todos</h1>
<button onClick={createTodo}>+ new</button>
<ul>
{todos.map((todo) => (
<li
onClick={() => deleteTodo(todo.id)}
key={todo.id}>{todo.content}</li>
))}
</ul>
<div>
🥳 App successfully hosted. Try creating a new todo.
<br />
<a href="https://docs.amplify.aws/react/start/quickstart/#make-frontend-updates">
Review next step of this tutorial.
</a>
</div>
<button onClick={signOut}>Sign out</button>
</main>
);
}
export default App;
ついでにドキュメントを参考にサインイン画面を日本語化しておきます。また、こちらのドキュメントのとおりサインアップの項目を隠すこともできるようなので併せて適用してみます。
import React from "react";
import ReactDOM from "react-dom/client";
import { Authenticator } from '@aws-amplify/ui-react';
import App from "./App.tsx";
import "./index.css";
import { Amplify } from "aws-amplify";
import outputs from "../amplify_outputs.json";
import '@aws-amplify/ui-react/styles.css';
import { I18n } from 'aws-amplify/utils';
import { translations } from '@aws-amplify/ui-react';
I18n.putVocabularies(translations);
I18n.setLanguage('ja');
Amplify.configure(outputs);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Authenticator hideSignUp>
<App />
</Authenticator>
</React.StrictMode>
);
サンドボックス環境を実行中の状態でここまでのコードを保存します。サンドボックスに内容がデプロイされ、完了するとターミナルに以下のように出力されているかと思います。
✅ amplify-amplifyvitereacttemplate-nishimuranaoki-sandbox-ae047ccccc
✨ Deployment time: 69.38s
Outputs:
(略)
amplify-amplifyvitereacttemplate-nishimuranaoki-sandbox-ae047ccccc.oauthCognitoDomain = test781f9d3a03ecf4bc.auth.ap-northeast-1.amazoncognito.com
(略)
上記の「oauthCognitoDomain」の値を控えておきます。
Meta for Developersへの連携
[ユースケース] - [カスタマイズ]を選択します。
emailは必須の情報となるのでアクションから[追加]を押しておきます。
[有効なOAuthリダイレクトURI]にhttps://(先ほど控えたoauthCognitoDomainの値)/oauth2/idpresponse
を入力して[変更を保存]を押します。
なお、複数のサンドボックスや本番デプロイで同じFacebookログインを使用する場合は以下のように複数入力します。
Google Cloudへの連携
作成した[クライアント]を選択して、以下のとおり入力して[保存]します。
- 承認済みの JavaScript 生成元:
https://(先ほど控えたoauthCognitoDomainの値)
- 承認済みのリダイレクト URI:
https://(先ほど控えたoauthCognitoDomainの値)/oauth2/idpresponse
動作確認
サンドボックス環境を実行中の状態でアプリケーションを起動します。
$ npm run dev
> amplify-vite-react-template@0.0.0 dev
> vite
VITE v5.4.10 ready in 5564 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
ブラウザでhttp://localhost:5173/
にアクセスすると以下のように表示されました。
[Googleでサインイン]を選択すると見慣れた画面が表示されました。
[Facebookでサインイン]を選択すると見慣れ...てはいないのですが、同じように確認画面が表示されました。
Googleでサインインしてコメントを登録してみました。苗字も問題なく表示されています。
同様にFacebookでサインインしてコメントを登録してみました。上記で登録したコメント「Googleでサインイン」は表示されていません。
GoogleアカウントとFacebookアカウントのそれぞれのサインインでは別々のユーザとして認識されています。
Cognitoのユーザープールで確認すると以下のとおり登録されています。
Googleユーザーの方を開くと、以下のとおりユーザー属性が登録されていることが分かります。Googleアカウントでサインインしたグループにも自動的に追加されるようです。
補足(サンドボックス環境ではなくAmplifyアプリとしてデプロイする場合)
修正したコードをpushしてAmplifyのアプリとしてデプロイする場合は、事前にシークレットを登録しておかないとデプロイが失敗しますのでご注意ください。
サンドボックス環境ではCLIで設定しましたが、Amplifyアプリの場合は以下のようにマネジメントコンソール画面から設定可能です。
また、Amplifyアプリとしてデプロイする場合はサンドボックス環境のようにCloudFormationのOutputsが表示されないので、外部IdPに登録するOAuthリダイレクトURIを別途確認する必要があります。
Cognitoの画面からであれば以下のように[ドメイン]から確認することできます。
なお、上記についてはAmplifyアプリやサンドボックスが複数登録されているアカウントではどれが対象のユーザープールなのか分かりにくいかと思います。
このような場合には、以下のようにAmplifyのマネジメントコンソールからユーザープールを開くか、amplify_outputs.json
内にも記載されているのでそちらから確認することもできます。
以下のようにCLIでCloudFormationのOutputsを参照することも可能です。
### 対象のAmplifyアプリのIDを確認
$ aws amplify list-apps --query 'apps[].[name,appId]' --output table
---------------------------------------------------------
| ListApps |
+--------------------------------------+----------------+
| private-amplify-vite-react-template | d9id5pj81gla |
+--------------------------------------+----------------+
### 確認したアプリIDを使用してデプロイに使用されたスタック名を取得
$ aws amplify list-branches --app-id d9id5pj81gla --query 'branches[].backend.stackArn' --output text | awk -F'/' '{print $2}'
amplify-d9id5pj81gls-main-branch-0b3c495192
### 対象のOutputs(oauthCognitoDomain)を取得
$ aws cloudformation describe-stacks --stack-name amplify-d9id5pj81gls-main-branch-0b3c495192 --query "Stacks[0].Outputs[?OutputKey=='oauthCognitoDomain'].OutputValue" --output text
result4a5b43652256dd.auth.ap-northeast-1.amazoncognito.com
そもそもデフォルトのCognitoドメインを使用せずにカスタムドメインをdefineAuthで定義できれば確認する必要はないのでは?とも思ったのですが、残念ながら本記事の執筆時点ではCognitoのドメインでカスタムドメインを設定することはdefineAuthではできないようです。
まとめ
Amplifyを使用して外部IdPによるサインインを試してみました。細かく設定するにはCognitoや外部IdP側の理解が必要ですし、それをどうやってコードで記述するのかについてはAmplifyの理解も必要ですが、サンドボックスを活用しながらコードの記述だけでバックエンドリソースを定義できるのは非常に便利だと思いました。
本記事がどなたかのお役に立てれば幸いです。