NextAuthなし・APIルートなしで実装:Next.js + AWS Amplify だけでメール/パスワード認証を完結する方法
要約:Next.js の app/page.tsx 一枚だけで、AWS Amplify(Authモジュール)を使い、Amazon Cognito ユーザプールのメール/パスワード認証(SRP)をフロントエンド完結で実装する手順を解説します。NextAuth も API ルートも不要で、ログイン/ログアウト、トークンの取得、初回パスワード変更(NEW_PASSWORD_REQUIRED)対応、IDトークンを使ったAPI呼び出しのポイント、つまずきやすいエラー対処までまとめました。
1. なぜ Amplify だけでいいのか
AWS Amplify の Auth モジュールは、Cognito ユーザプールの SRP 認証をブラウザで安全に実行できます。これにより、NextAuth などのサーバ側セッションを用意しなくても、フロントエンドでログイン状態を保持し、ID/Access トークンを取得できます。API と連携する場合はトークンをヘッダーに付与すればよく、フロント完結で MVP を素早く構築できます。
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}` },
});
7. よくあるエラーと対処
-
「Incorrect username or password.」:単純な打ち間違いのほか、ユーザーが未確認(Unconfirmed)の可能性。サインアップ時のメール確認を完了させる。
-
「User is not confirmed.」:確認コードで
confirmSignUpを呼ぶ処理が必要。 -
初回チャレンジ(NEW_PASSWORD_REQUIRED):上記
confirmSignInの UI を追加。 -
環境変数が空:
NEXT_PUBLIC_AWS_COGNITO_*がundefinedだと初期化で失敗。ビルドログで確認。 -
リージョン/Pool/ClientId の不一致:ユーザプールIDとクライアントIDの組み合わせ、リージョンを再確認。
8. セキュリティ&運用の注意点
-
トークンの保管:Amplify はブラウザストレージに保持します。公開端末ではログアウトを促し、期限切れハンドリング(再サインイン)を計画。
-
SSR 保護が必要か:完全フロント完結は構築が速い反面、厳密な SSR ガードが難しい。必要なら NextAuth 等を併用。
-
CORS と HTTPS:API 側は CORS 設定・HTTPS を正しく構成。トークンの送信は常に HTTPS 経由で。
9. FAQ
Q. サインアップ/確認メールも Amplify だけでできますか?
A. 可能です。signUp と confirmSignUp
を使えば同様にフロント完結で実装できます。
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の挙動を徹底解説
SEO対策に必須!検索上位を狙うsitemap.xmlの完全ガイド
Google Search Consoleの「代替ページ(適切なcanonicalタグあり)」とは?原因と解決法を徹底解説!
Microsoft純正の新しいコンソールエディタ「edit」が復活!| edit.exe インストール方法
Googleサイト確認のTXTレコードをnslookupで確認する方法【SEO対策】
Googleサーチコンソールに反映されるための最低限のSEO構造とは
【Anker Soundcore Liberty 4】イヤーピース紛失!代替品はAmazonで購入
JavaScriptでタイムゾーン変換!UTCとJST(日本時間)の変換方法
git switchの使い方とgit checkoutとの違い
HTMLとJavaScriptモジュールでクラスを定義し、ボタンから呼び出す方法
JavaScriptでTensorFlow.jsを動的に読み込む方法|HTMLに直接書かずに機械学習を実行する
ffmpegでMOVファイルを逆再生する方法【音声付き対応】
Windows 11でタスクマネージャー以外からアプリを終了させる方法【PowerShell・コマンドプロンプト】
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(静的書き出し)比較