Next.js + AWS Cognitoによる認証トークン管理
ここ最近のプロジェクトで、AWS Cognitoを使用して認証されたユーザーのみページを閲覧できるNext.jsアプリケーションに関わることがありました。その時の認証情報の管理についてご紹介します。
サーバーサイドでリダイレクトを制御する
アプリケーションの要件として、認証されていないユーザーはどのページにアクセスしても/login
にリダイレクトすることになっています。
一つの方法としてuseEffect Hookを使いクライアントサイドでリダイレクトを行う方法[1]がありますが、その性質上、一瞬描画された画面が見えてしまうためあまり好ましくありません。
サーバーサイドでリダイレクトする場合、getServerSideProps
、getInitialProps
を使う手法がありますが、aiji42/next-fortressというライブラリがシンプルかつ使いやすく感じました。
Firebase、Auth0、そして今回使うCognitoなどの認証をサポートし、ミドルウェアに設定を加えることで非認証状態の場合は指定した箇所にリダイレクトさせることが出来ます。
このアプリケーションでは全てのページでリダイレクト判定を行うため、/pages
ディレクトリの直下にmiddlewareを配置しました。
import { makeCognitoInspector } from 'next-fortress'
// Cognitoに関する環境変数
const region = process.env.NEXT_PUBLIC_COGNITO_REGION ?? ''
const userPoolId = process.env.NEXT_PUBLIC_COGNITO_USER_POOL_ID ?? ''
const userPoolWebClientId =
process.env.NEXT_PUBLIC_COGNITO_USER_POOL_WEB_CLIENT_ID ?? ''
export const middleware = makeCognitoInspector(
{ type: 'redirect', destination: '/login' },
{ region, userPoolId, userPoolWebClientId }
)
これにより、各ページにアクセスする度にmiddlewareが走り、Cognitoの認証が通っていない場合は/login
ページにリダイレクトされます。
しかし、この方法には2つの問題点があります。
問題点1: LocalStorageにtokenが保存される
フロントサイドではAWS Amplifyフレームワークを使用してCognitoとアプリケーションを統合することが出来ますが、デフォルトで認証情報がLocalStorageに保存されてしまいます。
仮にアプリケーションに脆弱性が見つかり、攻撃者からXSSによってスクリプトを実行された場合、token情報が外部に漏れる恐れがあります。JSからトークン情報へのアクセスを防ぐためにHTTP-Only属性を持つCookieに情報を載せる必要がありました。
この問題についてはIssueで議論されており[2]、有志によってnitrictech/amplify-secure-jsのようなライブラリが開発されています。
独自のstorageクラスを定義し、HTTP Cookieへの移行を行うことができます。
import { amplifyLocalStorage } from "@nitric/amplify-secure-js";
import config from "./aws-amplify-config";
Amplify.configure({
...config,
storage: amplifyLocalStorage,
});
問題点2: API Routesの実行が止められる
今回の方法ではmiddlewareが/pages
ディレクトリ以下の全てのページに影響するため、/pages/api
にあるようなAPI エンドポイントへのリクエスト自体も認証されていない限り止められてしまいます。
認証プロセスの途中でAPIの処理を挟みたい場合は、次のように特定のAPIのみ実行を許可するよう調整する必要があります。
const makeCognitoInspector = (
fallback: Fallback,
params: UserPoolParams,
customHandler?: (payload: any) => boolean
) => {
return async(request: NextRequest, event: NextFetchEvent) => {
const ok = await verifyCognitoAuthenticatedUser(
request,
params,
customHandler
)
// CookieにCognito認証情報があるか、認証に関するAPIの実行であればfallbackを実行しない
if (ok || request.page.name?.includes('/api')) return
return handleFallback(fallback, request, event)
}
}
まとめ
認証系をシンプルに組み込めるCognitoですが、その仕様を正しく知った上で使わなければ重要なセキュリティリスクを負う可能性があります。Cognitoに限らず、自分が使うサービスがどのような仕組みで情報を管理するのか把握した上でセキュリティ問題の対策をするように気をつけましょう。
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion