🏷️

Cognitoの認証・トークン管理のメモ

に公開

はじめに

AWS Amplifyにデプロイしたアプリケーションでは、認証機能でCognitoを使う場面が多いかと思います。
Cognitoの認証フローやトークン管理の設定を行う際に、毎回調べ直しているので、ざっくりとですが整理してみました。
※情報に間違いがありましたらご指摘いただけますと幸いです🙇

ユーザー / 権限管理

認証の入り口(IdP)が何であっても、ユーザーはCognitoのユーザーとして管理される

  • Google等でログインした際に、そのユーザー情報を自身のユーザープール内に複製する。
  • Cognito側から見れば、ユーザーは一意のID(sub)が割り当てられた存在として、等しく管理される。

これらのユーザーをグループに追加し、そのグループにIAロールを紐づければ、RBACで権限管理が可能になる。
→ 外部IdP側でユーザーの削除をするとCognitoのユーザープール内にも反映されるので、セキュリティ管理は外部IdP側に任せられる。

トークン管理

  • ユーザープールの直接ログイン
  • 外部IdPと連携してログイン

    認証パターンを変更してもトークン保存の仕組みは変わらない

Cognitoからトークンが払い出されるタイミングは、認証フローによって異なる。

  1. 通常のユーザープール認証

    • ID/Password送信 -> Cognitoによる検証 -> Cognitoトークン発行
  2. 外部IdP連携

    • 外部IdP認証 -> 外部Idp側でトークン発行 -> Cognitoによる検証 -> Cognitoトークン発行

※外部Idpのトークンを直接扱うことは基本的にはなく、Cognitoが発行したトークン(JWT)を使ってAPIを呼び出したり、画面制御を行う。

トークンの保存方法

参考: Tokens and credentials

  • Local Storage
  • Cookie Storage
  • Session Storage
  • Custom Storage

Local 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を配置するルートハンドラーを構成する。

参考: Server-Side Rendering

(機械翻訳)
サーバーサイドサインイン機能を有効にすると、認証トークンはHttpOnly Cookieに保存され、HttpOnly属性を変更できなくなります。これらのCookieはクライアントサイドのスクリプトからアクセスできないため、クライアントサイドでAmplify JS APIを使用できなくなります。したがって、クライアントサイドでAmplifyを設定する必要はありません。これらのAmplify JSサーバーサイドAPIはサーバーサイドで引き続きご利用いただけます。

まとめ

Next.js、Amplifyのドキュメントは比較的読みやすく解説も丁寧なので、もし実装される際はまず一読をお勧めしたいです。
色々とAWSリソースを作成し、ロジックを組み立てた後にやり直しになるのは辛いですからね・・・😢

Discussion