Cognitoの認証・トークン管理のメモ
はじめに
AWS Amplifyにデプロイしたアプリケーションでは、認証機能でCognitoを使う場面が多いかと思います。
Cognitoの認証フローやトークン管理の設定を行う際に、毎回調べ直しているので、ざっくりとですが整理してみました。
※情報に間違いがありましたらご指摘いただけますと幸いです🙇
ユーザー / 権限管理
認証の入り口(IdP)が何であっても、ユーザーはCognitoのユーザーとして管理される
- Google等でログインした際に、そのユーザー情報を自身のユーザープール内に複製する。
- Cognito側から見れば、ユーザーは一意のID(sub)が割り当てられた存在として、等しく管理される。
これらのユーザーをグループに追加し、そのグループにIAロールを紐づければ、RBACで権限管理が可能になる。
→ 外部IdP側でユーザーの削除をするとCognitoのユーザープール内にも反映されるので、セキュリティ管理は外部IdP側に任せられる。
トークン管理
- ユーザープールの直接ログイン
- 外部IdPと連携してログイン
→
認証パターンを変更してもトークン保存の仕組みは変わらない
Cognitoからトークンが払い出されるタイミングは、認証フローによって異なる。
-
通常のユーザープール認証
- ID/Password送信 -> Cognitoによる検証 -> Cognitoトークン発行
-
外部IdP連携
- 外部IdP認証 -> 外部Idp側でトークン発行 -> Cognitoによる検証 -> Cognitoトークン発行
※外部Idpのトークンを直接扱うことは基本的にはなく、Cognitoが発行したトークン(JWT)を使ってAPIを呼び出したり、画面制御を行う。
トークンの保存方法
Local StorageCookie StorageSession StorageCustom Storage
Local Storage
ブラウザ内ストレージにトークンを保存する。
保持期間は永続であり、削除しない限り使い続けることができる。
※デフォルトの設定
Cookie Storage
Cookieにトークンが入るようになると、Next.jsのサーバーサイド機能であるProxyでリダイレクトさせることができる。
※Cookieとサーバーに関して:HTTP Cookie の使用
日本語版ドキュメント: proxy.js
proxy.js|tsファイルは、Proxyを記述し、リクエストが完了する前にサーバー上でコードを実行するために使用されます。受け取るリクエストに基づいて、書き換え、リダイレクト、リクエストまたはレスポンスヘッダーの変更、または直接レスポンスを返すことで、レスポンスを変更できます。
設定例
cognitoUserPoolsTokenProvider.setKeyValueStorage(new CookieStorage({
// アプリのドメインを指定(localhostの場合はそのままでOK)
domain: typeof window !== 'undefined' ? window.location.hostname : 'localhost',
// 本番環境(HTTPS)では"true", 開発環境(localhost)では"false"のように切り替え可能
secure: process.env.NODE_ENV === 'production',
// アプリケーションのルートパス
path: '/',
// CSRF対策
sameSite: 'strict'
}));
-
httpOnly:
上記のCookieStorageは、AmplifyのクライアントSDKがトークンを読み書きする必要があるため、httpOnly: false(JSからアクセス可能)なCookieを生成する。
これにより、LocalStorageと同様にクライアント側で認証状態を維持しつつ、Next.jsのサーバー側(SSR/Proxy)へも自動的にトークンが送信されるようになる。 -
secure:
本番環境では必須かも。HTTPS通信でのみCookieが送信される。
ローカル開発(localhost)は通常HTTPなので、ここを"true"にするとCookieが保存されず、ログインできない事象が発生する。
そのため、上記コードのように環境変数で切り替えるのが良さそう。
Session Storage
トークンはブラウザに保存され、 セッションはブラウザが開いている限り持続する。
タブが閉じられると消去される。
Custom Storage
開発者が用意した任意のロジックでトークンを保存する方式。
Amplifyが定めるインターフェース(getItem, setItem, removeItem 等)を実装したクラスを渡す必要がある。
設定例
// 自分でクラスを作る
class MyStorage implements KeyValueStorageInterface {
storage = new Map<string, string>();
async setItem(key: string, value: string) { this.storage.set(key, value); }
async getItem(key: string) { return this.storage.get(key); }
// ...他メソッド
}
cognitoUserPoolsTokenProvider.setKeyValueStorage(new MyStorage());
XSS(クロスサイトスクリプティング)の対策
Local Storage / Session Storage / Cookie Storage(HttpOnly:false)では、XSSでトークンを盗まれる可能性がある。
対策としては、CSP(Cross-site Scripting Prevention)の導入が推奨されているように見受けられる。
あとは、dangerouslySetInnerHTMLを使わないこと。
next.config.js|tsファイルで設定可能。
参考: How to set a Content Security Policy (CSP) for your Next.js application
HttpOnly: trueにしたら?
クライアントサイド(ブラウザ)で動く Amplify.configure()やaws-amplify/storageなどは、ブラウザのストレージ(CookieやLocalStorage)からトークンを取り出し、HTTPヘッダーにセットしてAWSへリクエストを送ろうとする。
つまり、HttpOnly: trueになると、JavaScriptからトークンが一切見えなくなるため、これらのライブラリは"未ログイン状態"と判定し、機能しなくなる。
そのため、アーキテクチャをBFF(Backend For Frontend)パターンに変更する必要がある。
APIに関しては、"aws-amplify/storage/server"のようなサーバーサイドに対応しているものを使い、一部のロジックはAWS SDKに切り替える。
Next.jsのAppRouterを選択している場合、"/api"配下のディレクトリにroute.tsを配置するルートハンドラーを構成する。
(機械翻訳)
サーバーサイドサインイン機能を有効にすると、認証トークンはHttpOnly Cookieに保存され、HttpOnly属性を変更できなくなります。これらのCookieはクライアントサイドのスクリプトからアクセスできないため、クライアントサイドでAmplify JS APIを使用できなくなります。したがって、クライアントサイドでAmplifyを設定する必要はありません。これらのAmplify JSサーバーサイドAPIはサーバーサイドで引き続きご利用いただけます。
まとめ
Next.js、Amplifyのドキュメントは比較的読みやすく解説も丁寧なので、もし実装される際はまず一読をお勧めしたいです。
色々とAWSリソースを作成し、ロジックを組み立てた後にやり直しになるのは辛いですからね・・・😢
Discussion