NextAuthなし・APIルートなしで実装:Next.js + AWS Amplify だけでメール/パスワード認証を完結する方法

2025-10-16

要約:Next.js の app/page.tsx 一枚だけで、AWS Amplify(Authモジュール)を使い、Amazon Cognito ユーザプールメール/パスワード認証(SRP)をフロントエンド完結で実装する手順を解説します。NextAuth も API ルートも不要で、ログイン/ログアウト、トークンの取得、初回パスワード変更(NEW_PASSWORD_REQUIRED)対応、IDトークンを使ったAPI呼び出しのポイント、つまずきやすいエラー対処までまとめました。

目次
  1. なぜ Amplify だけでいいのか
  2. 前提条件(Cognito 側の設定)
  3. プロジェクト準備(パッケージ&環境変数)
  4. 最小コード:app/page.tsx だけでログイン完結
  5. 初回パスワード変更(NEW_PASSWORD_REQUIRED)の扱い
  6. IDトークンでAPI連携する方法
  7. よくあるエラーと対処
  8. セキュリティ&運用の注意点
  9. FAQ

1. なぜ Amplify だけでいいのか

AWS Amplify の Auth モジュールは、Cognito ユーザプールの SRP 認証をブラウザで安全に実行できます。これにより、NextAuth などのサーバ側セッションを用意しなくても、フロントエンドでログイン状態を保持し、ID/Access トークンを取得できます。API と連携する場合はトークンをヘッダーに付与すればよく、フロント完結で MVP を素早く構築できます。

注意:SSR で厳格に保護されたページを実装したい場合や、バックエンドでのセッション管理が必要な場合は、NextAuth や自前のバックエンド層の導入を検討してください。

2. 前提条件(Cognito 側の設定)

ユーザプールのアプリクライアントで USER-SRP-Auth を有効化します。Hosted UI は不要です(使ってもOKですが本記事は非依存)。ユーザーは事前に作成済み、またはサインアップ機能を別途用意してください。

3. プロジェクト準備(パッケージ&環境変数)

pnpm add aws-amplify

.env.local

NEXT_PUBLIC_AWS_COGNITO_REGION=ap-northeast-1
NEXT_PUBLIC_AWS_COGNITO_USER_POOL_ID=ap-northeast-1_XXXXXXX
NEXT_PUBLIC_AWS_COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx

4. 最小コード:app/page.tsx だけでログイン完結

以下を app/page.tsx に置くだけで、ログイン/ログアウト・トークン取得まで動きます。

"use client";

import { useEffect, useState } from "react";
import { Amplify } from "aws-amplify";
import {
  signIn,
  signOut,
  getCurrentUser,
  fetchAuthSession,
  // confirmSignIn, // NEW_PASSWORD_REQUIRED に対応するなら後述を参照
  type SignInOutput,
} from "aws-amplify/auth";

Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId: process.env.NEXT_PUBLIC_AWS_COGNITO_USER_POOL_ID!,
      userPoolClientId: process.env.NEXT_PUBLIC_AWS_COGNITO_CLIENT_ID!,
      region: process.env.NEXT_PUBLIC_AWS_COGNITO_REGION!,
    },
  },
});

export default function Page() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [me, setMe] = useState<null | { username: string; idToken?: string }>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [nextStep, setNextStep] = useState<string | null>(null);

  useEffect(() => {
    (async () => {
      try {
        const user = await getCurrentUser();
        const session = await fetchAuthSession();
        setMe({
          username: user.username,
          idToken: session.tokens?.idToken?.toString(),
        });
      } catch {
        setMe(null);
      }
    })();
  }, []);

  const handleLogin = async () => {
    setError(null);
    setLoading(true);
    setNextStep(null);
    try {
      const res: SignInOutput = await signIn({ username: email, password });
      if (res.isSignedIn) {
        const session = await fetchAuthSession();
        setMe({ username: email, idToken: session.tokens?.idToken?.toString() });
      } else {
        const step = res.nextStep?.signInStep;
        setNextStep(step ?? "追加ステップが必要です");
        // NEW_PASSWORD_REQUIRED 等は後述
      }
    } catch (e: any) {
      setError(e?.message ?? "サインインに失敗しました");
    } finally {
      setLoading(false);
    }
  };

  const handleLogout = async () => {
    await signOut();
    setMe(null);
  };

  return (
    <main className="max-w-md mx-auto py-10">
      <h1 className="text-2xl font-bold mb-6">Cognito ログイン(Amplifyのみ)</h1>

      {!me ? (
        <>
          <div className="mb-4">
            <label className="block text-sm font-medium text-gray-700 mb-2">メールアドレス</label>
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
              className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
              placeholder="example@email.com" />
          </div>

          <div className="mb-6">
            <label className="block text-sm font-medium text-gray-700 mb-2">パスワード</label>
            <input type="password" value={password}
              onChange={(e) => setPassword(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && handleLogin()}
              className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
              placeholder="••••••••" />
          </div>

          {error && <p className="text-red-600 mb-4">{error}</p>}
          {nextStep && <p className="text-amber-700 mb-4">次のステップ: {nextStep}</p>}

          <button onClick={handleLogin} disabled={loading}
            className="w-full bg-indigo-600 text-white py-3 rounded-lg font-semibold hover:bg-indigo-700 transition disabled:opacity-60">
            {loading ? "ログイン中..." : "ログイン"}
          </button>
        </>
      ) : (
        <div className="space-y-4">
          <p className="text-green-700">ログイン中: <b>{me.username}</b></p>
          <details className="w-full p-3 border rounded">
            <summary className="cursor-pointer">ID トークンを見る</summary>
            <pre className="whitespace-pre-wrap break-all text-sm mt-2">{me.idToken ?? "(トークン未取得)"}</pre>
          </details>
          <button onClick={handleLogout}
            className="w-full bg-gray-800 text-white py-3 rounded-lg font-semibold hover:bg-gray-900 transition">
            ログアウト
          </button>
        </div>
      )}
    </main>
  );
}

5. 初回パスワード変更(NEW_PASSWORD_REQUIRED)の扱い

初回ログイン時などに res.nextStep.signInStep"CONFIRM_SIGN_IN_WITH_NEW_PASSWORD"(バージョンにより名称差異あり)として返ることがあります。この場合は UI を出して confirmSignIn(...) を呼びます。

import { confirmSignIn } from "aws-amplify/auth";

// 例:新しいパスワード newPw を受け取り、追加ステップを完了する
const handleCompleteNewPassword = async (newPw: string) => {
  try {
    const out = await confirmSignIn({ challengeResponse: newPw });
    if (out.isSignedIn) {
      const session = await fetchAuthSession();
      setMe({ username: email, idToken: session.tokens?.idToken?.toString() });
      setNextStep(null);
    } else {
      setNextStep(out.nextStep?.signInStep ?? "追加ステップが続いています");
    }
  } catch (e:any) {
    setError(e?.message ?? "パスワード更新に失敗しました");
  }
};

6. IDトークンでAPI連携する方法

ログイン後、fetchAuthSession() で ID/Access トークンを取得できます。API Gateway + Lambda(Cognito オーソライザー)等へは、Authorization: Bearer <idToken> を付与します。

const session = await fetchAuthSession();
const idToken = session.tokens?.idToken?.toString();

const res = await fetch("https://api.example.com/secure-endpoint", {
  method: "GET",
  headers: { Authorization: `Bearer ${idToken}` },
});
メモ: ID トークンは主にユーザー情報(クレーム)確認、Access トークンはAPI保護で使われることが多いです。バックエンド側のオーソライザー設定に合わせて使い分けてください。

7. よくあるエラーと対処

8. セキュリティ&運用の注意点

9. FAQ

Q. サインアップ/確認メールも Amplify だけでできますか?
A. 可能です。signUpconfirmSignUp を使えば同様にフロント完結で実装できます。

Q. Hosted UI(PKCE)を使うメリットは?
A. OAuth 連携や認可コードフローの標準化、UIの提供など。将来的なプロバイダ追加が容易です。

Q. 将来 NextAuth に移行したくなったら?
A. 可能です。Amplifyで得た要件・UXを保ちつつ、サーバ側セッションやSSR保護を追加できます。


まとめ:MVPやフロント主導のプロトタイプなら、Next.js + AWS Amplify(Auth)だけで Cognito 認証を完結できます。初回パスワード変更などのチャレンジは nextStep を見て分岐し、ID/Access トークンは fetchAuthSession() から取得して API と安全に連携しましょう。

関連リンク

.htaccessは階層で引き継がれる?Apacheの挙動を徹底解説

MBTI診断

SEO対策に必須!検索上位を狙うsitemap.xmlの完全ガイド

Google Search Consoleの「代替ページ(適切なcanonicalタグあり)」とは?原因と解決法を徹底解説!

Microsoft純正の新しいコンソールエディタ「edit」が復活!| edit.exe インストール方法

Googleサイト確認のTXTレコードをnslookupで確認する方法【SEO対策】

Googleサーチコンソールに反映されるための最低限のSEO構造とは

PHPで複数ファイルを順番に削除する方法

MBTI診断 | あなたの性格タイプを分析

PDFファイル結合|パスワード設定ツール

【Anker Soundcore Liberty 4】イヤーピース紛失!代替品はAmazonで購入

JavaScriptでタイムゾーン変換!UTCとJST(日本時間)の変換方法

pnpmとは?npmとの違いとインストール方法

git switchの使い方とgit checkoutとの違い

HTMLとJavaScriptモジュールでクラスを定義し、ボタンから呼び出す方法

JavaScriptでTensorFlow.jsを動的に読み込む方法|HTMLに直接書かずに機械学習を実行する

ChatGPT風リンクバッジをCSSで作成する

ffmpegでMOVファイルを逆再生する方法【音声付き対応】

Windows 11でタスクマネージャー以外からアプリを終了させる方法【PowerShell・コマンドプロンプト】

macOSでドメイン名から情報を取得する方法【初心者向け】

JavaScriptでPCの空き容量やメモリ量を取得できる?Chromeの制限と代替手法

PowerShellでNode.jsの最新バージョン一覧を確認する方法【Volta/Windows対応】

Next.jsでbasePathを/homepage2にしてS3へ静的デプロイする完全手順

NextAuthなし・APIルートなしで実装:Next.js + AWS Amplify だけでメール/パスワード認証を完結する方法

「Next.js pnpm build (ビルド)後の出力ファイルが見当たらない」トラブルシュート&静的デプロイ

AWS Amplify CLIでS3やZIPから手動デプロイ【実行コマンド付き】

GraphQL接続の落とし穴:Amplify Hosting(amplifyapp.com) vs S3+Next.js(静的書き出し)比較

Homebrewの「update」と「upgrade」の違い+バージョン確認方法|Macのパッケージ管理

.DS_StoreをGitでアップしない方法|macユーザー向け