pre-amble
まず cognito をなんのライブラリ経由で使うかの選択肢がいろいろあるようで、 AWS SDK を使うとか、 Amplify を使うとか、Amplify の auth 部分だけ切り出したライブラリを使うとか、調べるのが結構わかりにくかった。
現状の理解だと AWS SDK for Javascript v3 (v2 もあるけど v3 推奨) は各サービス (S3 とか DynamoDB とか) の API のラッパーで、一番低レイヤー。ちなみに npm i @aws-sdk/cognito
のようにサービスごとにインストールできる。
Amplify については、Amplify をまるっと使う場合と Amplify の認証部分だけ切り出した amazon-cognito-identity-js だけ使う場合がある。
もろもろの比較は多分ここを読んだほうがよさそう↓
AWS SDK について補足しておくと、cognito は1つの @aws-sdk/cognito
みたいにまとまっているわけではなく cognito-identity, cognito-identity-provider, cognito-sync の3つにわかれていて、ログイン・ユーザ登録をしたい場合は cognito-identity-provider を使う。しかもログインは xxSignInxx
みたいな自明なメソッドではなく InitiateAuthCommand
という名前のものを使わないといけない
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/cognito-identity-provider/command/InitiateAuthCommand/
紆余曲折あって、結局 Hosted UI を使うのがシンプルじゃない? となった。きっかけは↓のスライドを読んでイメージがつきやすかったのと、AWS SDK で実装していてコードを書くのが面倒になったこと。
Hosted UI
Hosted UI っていうのは、
- Cognito 側でログインの UI を用意していてくれて
- アプリケーション側ではこの UI のある URL にリダイレクトする ことで、認証を実現するもの
Hosted UI を使うか自前の UI を用意するかについてはこの記事がわかりやすそう。
注意点としては、Hosted UI は今のところ多言語対応をしていないので、どうしても日本語で表示させたいという要件がある場合は自前で UI の用意 + 前述のライブラリのなにかを使って認証を実装する必要がある。
また、Hosted UI は自分のアプリケーションの URL ではなく、AWS の URL にホストされていて URL は下記の通り。
`<COGNITO_DOMAIN>/login?client_id=<CLIENT_ID>&redirect_uri=<REDIRECT_URI>&response_type=code`
変数はどれも Cognito の管理画面で設定したもので、
- COGNITO_DOMAIN: user pool の cognito domain
- CLIENT_ID: app client の client ID
となる。
(redirect_uri はログインページのあるアプリケーションの URL)
処理の流れ
前提として、Cognito の認証は OAuth 2.0 のフローでやっていて、認可コードをもらってくるフローと、ID トークンをもらってくるフローの2つの実装が必要。
前者についてユーザを主語に記述すると、
- アプリのログインページに行く
- Hosted UI にリダイレクトされる
- 認証情報を入力してログインボタンを押す
- アプリに再度リダイレクトされて、ログイン状態になる
ここまでで認可コードがもらえるので、このあとに Cognito のトークンエンドポイントを叩いて ID トークンをもらってくる
実装: 全体のコード
事前準備としては
- react を https で動かす
- Cognito > User Pools > Application Client で、コールバック URL と sign out URL を https://localhost:3000 などアプリの URL に設定する
"scripts": { "start:https": "HTTPS=true react-scripts start", "start": "react-scripts start", ...
依存ライブラリは
- jwt-decode: ID トークンをデコードするために使う (ID トークンは JWT 形式だから。Auth0 のライブラリなのでよさそう)
- react router: クエリパラメータをとってくるために雑に入れた。別に react router でなくてもいい。
各種設定値 (REACT_APP_xx) は予め .env
ファイルか何かに入れておいてね。
import { jwtDecode } from "jwt-decode" import { useEffect, useState } from "react" import { useSearchParams } from "react-router-dom" type IDToken = { [key: string]: any } const getIdToken = async (code: string): Promise<IDToken | null> => { const endpoint = `${process.env.REACT_APP_COGNITO_DOMAIN}/oauth2/token` const headers = { "content-type": "application/x-www-form-urlencoded" } const body = new URLSearchParams({ grant_type: "authorization_code", client_id: process.env.REACT_APP_CLIENT_ID || "", code: code, redirect_uri: process.env.REACT_APP_REDIRECT_URI || "", }) const res = await fetch(endpoint, { method: "POST", headers, body, }) if (!res.ok) { return null } const data = await res.json() return jwtDecode(data.id_token) } const Root = () => { const [searchParams, _] = useSearchParams() const [idToken, setIdToken] = useState<IDToken>({}) // Hosted UI でログインしたあと、 code つきでリダイレクトされたときに実行するコード useEffect(() => { const code = searchParams.get("code") if (!code) { return } const f = async () => { console.log(code) const res = await getIdToken(code) if (!res) { return } setIdToken(res) } f() }, [searchParams]) // ボタンクリックしたときに Hosted UI にリダイレクトする // 初回レンダーで実行してもよさそう const redirectToLoginUI = () => { window.location.href = `${process.env.REACT_APP_COGNITO_DOMAIN}/login?client_id=${process.env.REACT_APP_CLIENT_ID}&redirect_uri=${process.env.REACT_APP_REDIRECT_URI}&response_type=code` } return ( <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> <button onClick={redirectToLoginUI}> ログイン </button> {Object.entries(idToken).map(([key, value]) => ( <pre>{`${key}: ${value} `} </pre> ))} </div> ) } export { Root }
実装: 認可コードをとってくる
ボタンを押したら Hosted UI の URL に遷移するようにした。別にボタンクリックがトリガーじゃなくてもいいと思う。
const redirectToLoginUI = () => { window.location.href = `${process.env.REACT_APP_COGNITO_DOMAIN}/login?client_id=${process.env.REACT_APP_CLIENT_ID}&redirect_uri=${process.env.REACT_APP_REDIRECT_URI}&response_type=code` }
<button onClick={redirectToLoginUI}>ログイン</button>
ドキュメントはここ: https://docs.aws.amazon.com/cognito/latest/developerguide/login-endpoint.html
実装するときは↓の記事を参考にして進めた
実装: ID トークンをとってくる
ユーザがリダイレクトした先の Hosted UI (=ログイン画面) で認証情報を入力すると、今度は Hosted UI から自分のアプリにまたリダイレクトされる。このとき、クエリパラメータに ?code=<認可コード>
が入ってくるので useEffect(...)
の部分が発火する。
(https://localhost:3000 → https://xxx.amazoncognito.com/login?... → https://localhost:3000?code=yyy)
// Hosted UI でログインしたあと、 code つきでリダイレクトされたときに実行するコード useEffect(() => { const code = searchParams.get("code") if (!code) { return } const f = async () => { console.log(code) const res = await getIdToken(code) if (!res) { return } setIdToken(res) } f() }, [searchParams])
パラメータに CODE が入っていた場合 ↓の getIdToken
で、トークンエンドポイントを叩く。ドキュメントはここで、https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html ヘッダーは何を使ってねとかいろいろ指定してある。
const getIdToken = async (code: string): Promise<IDToken | null> => { const endpoint = `${process.env.REACT_APP_COGNITO_DOMAIN}/oauth2/token` const headers = { "content-type": "application/x-www-form-urlencoded" } const body = new URLSearchParams({ grant_type: "authorization_code", client_id: process.env.REACT_APP_CLIENT_ID || "", code: code, redirect_uri: process.env.REACT_APP_REDIRECT_URI || "", }) const res = await fetch(endpoint, { method: "POST", headers, body, }) if (!res.ok) { return null } const data = await res.json() return jwtDecode(data.id_token) }
あとは ID トークンが state に入ってくるので好きにできる。