🔑

Next.js + AWS Cognitoによる認証トークン管理

2022/10/20に公開

ここ最近のプロジェクトで、AWS Cognitoを使用して認証されたユーザーのみページを閲覧できるNext.jsアプリケーションに関わることがありました。その時の認証情報の管理についてご紹介します。

サーバーサイドでリダイレクトを制御する

アプリケーションの要件として、認証されていないユーザーはどのページにアクセスしても/loginにリダイレクトすることになっています。

一つの方法としてuseEffect Hookを使いクライアントサイドでリダイレクトを行う方法[1]がありますが、その性質上、一瞬描画された画面が見えてしまうためあまり好ましくありません。

サーバーサイドでリダイレクトする場合、getServerSidePropsgetInitialPropsを使う手法がありますが、aiji42/next-fortressというライブラリがシンプルかつ使いやすく感じました。

Firebase、Auth0、そして今回使うCognitoなどの認証をサポートし、ミドルウェアに設定を加えることで非認証状態の場合は指定した箇所にリダイレクトさせることが出来ます。

このアプリケーションでは全てのページでリダイレクト判定を行うため、/pagesディレクトリの直下にmiddlewareを配置しました。

pages/_middleware.ts
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への移行を行うことができます。

公式README.mdより抜粋
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に限らず、自分が使うサービスがどのような仕組みで情報を管理するのか把握した上でセキュリティ問題の対策をするように気をつけましょう。

脚注
  1. https://zenn.dev/uttk/articles/4649e49f1e6628 ↩︎

  2. https://github.com/aws-amplify/amplify-js/issues/3224 ↩︎

chot Inc. tech blog

Discussion