🐙

【Cognito ユーザープール・Next.js】安全にCognito認証を実装する

に公開

はじめに

Cognitoユーザープールを使用したログイン画面の作成方法は、いくつか種類があります。
その中でもAmplify Authを使用した実装をいくつか試してみました。
また、Next.jsを使用し、セキュリティにも配慮した実装も行っていますのでご紹介します。

ライブラリの紹介

amazon-cognito-identity-js

概要
Cognito User Pool 専用の旧フロントエンド向けJavaScriptライブラリ
SRP認証を直接扱える
特徴
クライアント側でSRP認証を行える
コールバックベースAPIが多い
Amplify Authの内部で使われていた
新規開発では非推奨気味(Amplify Auth 推奨)
用途
旧SPAやレガシーコードでSRP認証を直接実装したい場合
Amplify を使いたくないがSRPを使いたい場合

@aws-sdk/client-cognito-identity-provider(SDK v3)

概要
AWS SDK for JavaScript v3 の一部
Cognito User Pool の API を低レベルで呼ぶためのライブラリ
SRPの計算は自分で行う必要あり
特徴
PromiseベースのモダンSDK
サーバーサイドでのユーザー管理(Admin系操作)に最適
サインインフローも呼べるが、SRPやトークン管理は自前で実装する必要がある
モジュール単位で軽量
用途
サーバーサイドでユーザー作成・削除・属性更新
カスタム認証フロー(Lambda連携)
バックエンド管理スクリプトや統合システム

Amplify Auth

概要
フロントエンド(React、Next.js、モバイルなど)向けに高レベルでCognito認証を簡単に使えるライブラリ。
内部的には amazon-cognito-identity-js や AWS SDK を使っている。
特徴
サインイン・サインアップ・MFA・パスワードリセット・SRP認証を自動で処理
TypeScript対応、Promiseベース
フロントエンド開発者が「ほぼ設定だけ」で使える
Hosted UI の利用も可能
SRP認証やトークン管理をライブラリが自動でやってくれる
用途
SPAやモバイルアプリでのユーザー認証
開発速度優先でセキュアにログイン機能を組みたい場合

Amplify Auth セキュリティに関して

トークン保管先について

Amplify Authのデフォルトでは、トークン保管先としてLocal Storageに保存されます。
Next.jsではAmplify.configure(amplifyConfig, { ssr: true })を設定することでCookieに保存することができます。
ただ、Local StorageCookieのどちらであっても、XSS対策が重要です。
https://www.docswell.com/s/ockeghem/ZM6VNK-phpconf2021-spa-security#p90
https://qiita.com/asw_hoggge/items/cac11b0d9b96c2bfc222
https://www.shadan-kun.com/waf_websecurity/xss/
https://zenn.dev/zuma_lab/articles/74a999aa1a8c59
https://zenn.dev/hikarucraft/articles/nextjs-first-guide-to-security
https://dev.classmethod.jp/articles/amplify-auth-get-user-info/

Next.jsではmiddlewareを使用することでHttpOnly Cookieを使用して認証することができます。
「HttpOnly」属性を設定されたCookieには、HTML内のスクリプトからはアクセスできないようになります。
そのため、仮にクロスサイトスクリプティング攻撃を受けたとしても、JavaScriptによるCookie情報の盗み出しを防止することができます。
https://www.aeyescan.jp/blog/cross-site-scripting/#toc12
https://zenn.dev/mabo23/articles/e4b980e61a0d47

■middleware・API Routesとは
Middleware は、ユーザーからのリクエストがサーバーに届いてから、Next.js がページや API ルートを処理する直前のタイミングで実行されるものです。
このタイミングでリクエストを検査し、必要に応じて内容を書き換えたり、別のページにリダイレクトしたりといった処理を差し込むことができます。
https://zenn.dev/cloud_ace/articles/a30c75dbe220f3
https://qiita.com/GleapPost/items/0b5c4e7425adfdbfaed6

環境変数について

Next.jsでは、NEXT_PUBLIC_プレフィックスを使用することで、Client側でもServer側でも読み込めるようになります。
amazon-cognito-identity-jsAmplify Authでのクライアントで認証する場合は、この環境変数が必要になります。
ただ、この環境変数は、ブラウザに送信されるJavaScriptにインライン化されます。
このインライン化はビルド時に行われるため、様々なNEXT_PUBLIC_環境はプロジェクトのビルド時に設定される必要があります。
次のように変数を利用した場合のインライン化されません。

// 変数を使用した指定はインライン化されない
const varName = 'NEXT_PUBLIC_ANALYTICS_ID'
setupAnalyticsService(process.env[varName])

// 変数を使用した指定はインライン化されない
const env = process.env
setupAnalyticsService(env.NEXT_PUBLIC_ANALYTICS_ID)

https://zenn.dev/hisayuki_mori/articles/environment-variables-for-nextjs

そもそもNEXT_PUBLIC_プレフィックスをつけずにmiddlewareAPI Routesを使用してサーバーサイドで環境変数を処理する実装も可能です。

Cognito側の設定

ここからは、Cognitoの設定に入ります。

サンプル実装

サインイン

  • ユーザー名
  • パスワード

サインアップ

  • ユーザー名
  • メールアドレス
  • パスワード

サインイン識別子のオプション

これは、ユーザーがログイン画面でIDとして入力できるものは何かを決める設定です。
各オプションの意味

  • メールアドレス
    ユーザーはログイン時にメールアドレス(例: user@example.com)を入力します。
    ユーザーID = メールアドレス、という構成になります。
  • 電話番号
    ユーザーはログイン時に電話番号(例: +819012345678)を入力します。
    ユーザーID = 電話番号、という構成になります。
  • ユーザー名
    ユーザーはサインアップ時に、メールアドレスや電話番号とは別に、自分で好きなユーザー名(例: taro_yamada123)を作成します。
    ログイン時には、このユニークなユーザー名を入力します。

サインアップのための必須属性

これは、ユーザーがアカウントを新規作成(サインアップ)するときに、必ず入力しなければならない項目は何かを決める設定です。
ここで選択した属性(例: email, phone_number, given_name(名)など)は、サインアップ画面で必須入力項目になります。
例えば「email」を必須属性にすると、ユーザーはメールアドレスを必ず入力しないとアカウントを作成できません。

https://dev.classmethod.jp/articles/study-tokens-of-cognito-user-pools/

認証フロー

フロー名 使い方 特徴
ALLOW_USER_AUTH(デフォルトオン) ユーザーが複数の認証方式から選択できる(パスワード、OTP、生体認証、MFAなど) 複数のサインイン手段を許可し、ユーザーがUIで選ぶ
ALLOW_USER_PASSWORD_AUTH ユーザー名+パスワードを直接送信してサインイン 実装がシンプルだが、パスワードが直接送られるためTLS必須
ALLOW_USER_SRP_AUTH(デフォルトオン) SRP(Secure Remote Password)プロトコルでサインイン パスワードを直接送らずに安全に検証できる。AWS推奨。
ALLOW_ADMIN_USER_PASSWORD_AUTH サーバー側でユーザー名+パスワードを使ってサインイン Admin APIでのみ使える。Hosted UIでは不可
ALLOW_CUSTOM_AUTH Lambdaトリガーでカスタム認証チャレンジを作る 独自のOTP、外部IdP、秘密質問などを組み込める
ALLOW_REFRESH_TOKEN_AUTH(デフォルトオン) Refresh Tokenを使って新しいトークンを取得 ユーザー入力なしでセッション更新可能

デフォルトオンとはアプリケーションを定義で シングルページアプリケーション (SPA) を選択したときのデフォルト

SRP(Secure Remote Password)方式とは

  • パスワードをネットワーク経由で直接送らない
  • クライアントとCognito間で暗号学的なやり取りをして、パスワードが漏れにくい
  • AWS公式ではSRPを推奨(ALLOW_USER_SRP_AUTH)

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow-methods.html
https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-security-best-practices.html?utm_source=chatgpt.com

サンプル実装

バージョン

  • Next.js:15.4.6
  • React:19.1.0
  • amazon-cognito-identity-js:6.3.15
  • aws-amplify:6.15.5

環境変数

.env.local
# AWS Cognito設定
NEXT_PUBLIC_USER_POOL_ID=ユーザープール ID
NEXT_PUBLIC_CLIENT_ID=クライアント ID

# AWS Cognito設定(middleware,API Routes使用時)
USER_POOL_ID=ユーザープール ID
CLIENT_ID=クライアント ID
AWS_REGION=ap-northeast-1

amazon-cognito-identity-js の実装例

npm install amazon-cognito-identity-js
###バージョン###
amazon-cognito-identity-js: 6.3.15
client-ver/
└── src/
    └── app/
        ├── components/
        │   ├── AuthWrapper.tsx      # 認証チェック用ラッパーコンポーネント
        │   ├── LogoutButton.tsx      # ログアウトボタンコンポーネント
        │   └── UserInfo.tsx          # ユーザー情報表示コンポーネント
        │
        ├── login/
        │   └── page.tsx              # ログインページ
        │
        ├── signup/
        │   └── page.tsx              # サインアップ(新規登録)ページ
        │
        ├── globals.css               # グローバルCSS(Tailwind設定含む)
        ├── layout.tsx                # ルートレイアウト
        └── page.tsx                  # トップページ
//認証フロー図
ユーザー → ログインページ
         ↓
    Cognito認証
         ↓
    トークン取得
         ↓
  localStorage保存
         ↓
    TOPページへ
         ↓
  AuthWrapperで保護

1. 認証ラッパー

AuthWrapper.tsx
// 全ページで認証状態をチェック
useEffect(() => {
  const idToken = localStorage.getItem('idToken')
  if (!idToken) {
    router.push('/login')
  }
}, [router])
  • クライアントコンポーネントとして実装
  • localStorageのトークンをチェック
  • 未認証の場合は自動的にログインページへリダイレクト

2. ログインページ

login/page.tsx
cognitoUser.authenticateUser(authenticationDetails, {
  onSuccess: (result: CognitoUserSession) => {
    // 3つのトークンを保存
    localStorage.setItem('idToken', result.getIdToken().getJwtToken())
    localStorage.setItem('accessToken', result.getAccessToken().getJwtToken())
    localStorage.setItem('username', username)
    router.push('/')
  }
})
  • IDトークン - ユーザー情報を含むJWT
  • アクセストークン - APIアクセス用
  • ユーザー名 - 表示用

3. サインアップページ

signup/page.tsx
userPool.signUp(username, password, attributeList, [], callback)
  • ユーザー名、メールアドレス、パスワードを登録
  • 確認用パスワードとの一致チェック

4. ログアウト機能

LogoutButton.tsx
const handleLogout = () => {
  const cognitoUser = userPool.getCurrentUser()
  if (cognitoUser) {
    cognitoUser.signOut()  // Cognitoからサインアウト
  }
  // ローカルストレージをクリア
  localStorage.removeItem('idToken')
  localStorage.removeItem('accessToken')
  localStorage.removeItem('username')
  router.push('/login')
}

Amplify Auth の実装例

npm install aws-amplify
###バージョン###
aws-amplify: 6.15.5
client-ver-amplifyauth/
└── src/
    └── app/
        ├── components/
        │   ├── AmplifyProvider.tsx  # Amplify設定プロバイダー
        │   ├── AuthWrapper.tsx       # 認証チェック用ラッパーコンポーネント
        │   ├── LogoutButton.tsx      # ログアウトボタンコンポーネント
        │   └── UserInfo.tsx          # ユーザー情報表示コンポーネント
        │
        ├── lib/
        │   └── amplifyConfig.ts      # Amplify設定ファイル
        │
        ├── login/
        │   └── page.tsx              # ログインページ
        │
        ├── signup/
        │   └── page.tsx              # サインアップ(新規登録)ページ
        │
        ├── globals.css               # グローバルCSS(Tailwind設定含む)
        ├── layout.tsx                # ルートレイアウト
        └── page.tsx                  # トップページ
//認証フロー図
ユーザー → Amplify Auth → 自動トークン管理 → セキュアストレージ
                ↓
          自動リフレッシュ
                ↓
          セッション維持

1. Amplify設定

amplifyConfig.ts
export const amplifyConfig = {
  Auth: {
    Cognito: {
      userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
      userPoolClientId: process.env.NEXT_PUBLIC_CLIENT_ID!,
      signUpVerificationMethod: 'code' as const,
      loginWith: {
        username: true,
        email: false
      }
    }
  }
}

2. AmplifyProvider

AmplifyProvider.tsx
export default function AmplifyProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    Amplify.configure(amplifyConfig, { ssr: true })
  }, [])
  return <>{children}</>
}
  • Amplifyの初期化を一元管理
  • SSRサポートの有効化
  • レイアウトコンポーネントでラップ

3. 認証チェック

AuthWrapper.tsx
// 実際のセッション検証
try {
  await getCurrentUser()  // Cognitoと通信して検証
} catch {
  router.push('/login')
}
  • トークンの有効期限を自動チェック

4. ログイン処理

login/page.tsx
const { isSignedIn } = await signIn({ username, password })
if (isSignedIn) {
  localStorage.setItem('username', username)  // 表示用のみ保存
  router.push('/')
}
  • トークン管理が自動化

5. サインアップ処理

signup/page.tsx
// ステップ1: サインアップ
await signUp({
  username,
  password,
  options: {
    userAttributes: { email }
  }
})

// ステップ2: 確認
await confirmSignUp({
  username,
  confirmationCode: verificationCode
})

6. ログアウト機能

LogoutButton.tsx
  const handleLogout = async () => {
    try {
      await signOut()
      localStorage.removeItem('username')
      router.push('/login')
    } catch (error) {
      console.error('ログアウトエラー:', error)
    }
  }

Amplify Auth の実装例(Amplify Auth・middleware・API Routess)

  • 環境変数にNEXT_PUBLIC_を使用しない
  • リダイレクト処理はmiddleware実装
  • 認証はクライアント
npm install aws-amplify
###バージョン###
aws-amplify: 6.15.5
client-amplify-auth-apiroute/
└── src/
    ├── app/
    │   ├── api/
    │   │   └── config/
    │   │       └── route.ts         # Amplify設定を返すAPIエンドポイント
    │   │
    │   ├── components/
    │   │   ├── AmplifyProvider.tsx  # Amplify設定プロバイダー(API経由で設定取得)
    │   │   ├── LogoutButton.tsx     # ログアウトボタンコンポーネント
    │   │   └── UserInfo.tsx         # ユーザー情報表示コンポーネント
    │   │
    │   ├── login/
    │   │   └── page.tsx              # ログインページ
    │   │
    │   ├── signup/
    │   │   └── page.tsx              # サインアップ(新規登録)ページ
    │   │
    │   ├── globals.css               # グローバルCSS(Tailwind設定含む)
    │   ├── layout.tsx                # ルートレイアウト
    │   └── page.tsx                  # トップページ(ホームページ)
    │
    └── middleware.ts                 # 認証チェック用ミドルウェア
//認証フロー図
ユーザー → Amplify Auth → 自動トークン管理 → Cookie保存
                ↓
          自動リフレッシュ
                ↓
          セッション維持

//認証チェック
ページアクセス → Middleware → Cookie確認 → アクセス許可/拒否

1. 環境変数の保護

api/config/route.ts
// api/config/route.ts
export async function GET() {
  const config = {
    Auth: {
      Cognito: {
        userPoolId: process.env.USER_POOL_ID!,      // NEXT_PUBLIC_不要
        userPoolClientId: process.env.CLIENT_ID!,   // サーバーサイドのみ
        region: process.env.AWS_REGION!
      }
    }
  }
  return NextResponse.json(config)
}
  • クライアントバンドルに環境変数が含まれない
  • .env.localの内容が露出しない

2. Middleware による認証制御

middleware.ts
export function middleware(request: NextRequest) {
  // Amplifyが保存するCookieの形式を理解している
  const clientId = process.env.CLIENT_ID
  const lastAuthUser = request.cookies.get(
    `CognitoIdentityServiceProvider.${clientId}.LastAuthUser`
  )
  const idToken = request.cookies.get(
    `CognitoIdentityServiceProvider.${clientId}.${lastAuthUser?.value}.idToken`
  )

  if (!idToken) {
    // 未認証 → ログインページへ
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', pathname)
    return NextResponse.redirect(loginUrl)
  }
}

3. AmplifyProvider の動的設定

AmplifyProvider.tsx
// AmplifyProvider.tsx
useEffect(() => {
  const configureAmplify = async () => {
    // 1. サーバーから設定を取得
    const response = await fetch('/api/config')
    const config = await response.json()
    
    // 2. Amplifyを設定(SSRモード)
    Amplify.configure(config, { ssr: true })
  }
}, [])

4. ログインフロー

login/page.tsx
// login/page.tsx
const handleLogin = async () => {
  // 1. Amplify認証
  const { isSignedIn } = await signIn({ username, password })
  
  if (isSignedIn) {
    // 2. Amplifyが自動的にCookieに保存(ssr: true の効果)
    
    // 3. リダイレクト
    router.push(from)
    router.refresh() // Middlewareに認証状態を反映
  }
}

おわりに

CognitoユーザープールとNext.jsを組み合わせることで、安全かつ柔軟なユーザー認証が可能になります。
今回紹介したSRP認証、Amplify Auth、middlewareを用いたサーバーサイド認証などを理解し、セキュアなSPAやWebアプリケーションの開発に活用してください。

GitHubで編集を提案

Discussion