- Next.js
Next.jsの認証実装|Better Authでログインを作る完全コード
Next.js で認証を実装するいちばん手堅い方法は、認証ライブラリ(Better Auth など)に登録・ログイン・セッション管理を任せ、proxy.ts では Cookie の有無だけを確認し、ページと Server Action の中で本当のセッション検証をする構成です。 Next.js 公式の認証ガイドもこの「入口は楽観的チェック、データの近くで確実なチェック」という考え方を推奨しています。
この記事では、Next.js 16.3.6・Better Auth 1.7.6・SQLite(better-sqlite3)で、メール/パスワードの会員登録・ログイン・ログアウト・保護ページ・保護された Server Action までの完全なコードを掲載します。すべて next build → next start で動かし、Playwright(Chromium)で「登録 → 保護ページ → 名前変更 → ログアウト → 再ログイン」を実際に通した結果と、curl で確かめたエラー応答や Cookie の属性も載せています。Auth.js が Better Auth に移管された経緯と、Clerk・Supabase Auth を含めた選び方の比較表もまとめました。
Next.js 認証実装の早見表
| やりたいこと | この記事での実装 |
|---|---|
| 認証ライブラリ | Better Auth 1.7.6(新規なら Auth.js 側も Better Auth を推奨) |
| DB | SQLite(better-sqlite3)。本番は PostgreSQL・MySQL なども可 |
| テーブル作成 | npx auth@latest migrate(user・session・account・verification) |
| 認証 API の入口 | app/api/auth/[...all]/route.ts に toNextJsHandler(auth) |
| 会員登録・ログイン | Server Action から auth.api.signUpEmail / signInEmail |
| Server Action で Cookie を保存 | plugins: [nextCookies()](最後に置く) |
| ログアウト | auth.api.signOut → redirect('/login') |
| セッション取得(サーバー) | auth.api.getSession({ headers: await headers() }) |
| 同じ描画内の重複取得を防ぐ | React の cache() で包む |
| 未ログインを入口で振り分け | proxy.ts で getSessionCookie()(存在チェックのみ) |
| 保護ページの本当の検証 | ページの先頭で requireSession() |
| 保護された Server Action | Action の先頭でも requireSession() |
| エラーメッセージ | APIError を catch して useActionState で表示 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Next.jsの認証実装|Better Authでログインを作る完全コード」
認証ライブラリの比較(Better Auth・Auth.js・Clerk・Supabase Auth)
| 観点 | Better Auth | Auth.js(NextAuth.js) | Clerk | Supabase Auth |
|---|---|---|---|---|
| 形態 | ライブラリ(自分のアプリ内で動く) | ライブラリ | ホスティング型サービス | Supabase の機能(BaaS) |
| ユーザー情報の保存先 | 自分の DB | 自分の DB(アダプター)または JWT | Clerk 側 | Supabase の PostgreSQL |
| メール/パスワード | 標準で有効化できる | Credentials プロバイダーで自前実装寄り | 標準 | 標準 |
| ソーシャルログイン | 設定で追加 | 豊富なプロバイダー | 管理画面で追加 | 管理画面で追加 |
| 2FA・パスキー・組織 | プラグインで追加 | 自前実装寄り | 標準機能が多い | 一部標準 |
| ログイン画面 UI | 自作 | 自作(簡易画面あり) | 用意された UI 部品 | 自作(UI ライブラリあり) |
| 費用 | OSS(DB・サーバー費のみ) | OSS | 利用量に応じた料金プラン | 利用量に応じた料金プラン |
| npm(2026年9月時点) | better-auth 1.7.6 |
next-auth 4.24.15(latest)/ 5.0.0-beta.32(beta) |
@clerk/nextjs 7.9.7 |
@supabase/ssr 0.12.7 |
| 向いているケース | データを自社 DB に置きたい、機能を段階的に足したい | 既存の Auth.js プロジェクトの保守 | 認証 UI や運用をまとめて任せたい | DB やストレージも Supabase で揃える |
選び方の目安は、「ユーザーデータを自分の DB に持ちたいか」「ログイン画面や運用をどこまで任せたいか」の2軸です。 自分の DB で持ちたく、機能を少しずつ足したいなら Better Auth、画面や多要素認証の運用ごと任せたいなら Clerk、DB も含めて一式を揃えたいなら Supabase Auth が候補になります。
Auth.js が Better Auth に移管された経緯
2025年9月26日、Auth.js(旧 NextAuth.js)の GitHub で「Auth.js is now part of Better Auth」という発表がありました。発表で確認できた内容は次のとおりです。
| 項目 | 発表の内容 |
|---|---|
| 保守する人 | Better Auth のチームが Auth.js を保守する |
| 既存ユーザー | 使い続けてよい。セキュリティと緊急の問題への対応は続く |
| 新機能 | 活発な機能開発の予定はない |
| 新規プロジェクト | 多くのチームには Better Auth を推奨。移行ガイドを用意 |
| v5 について | Better Auth のメンテナーは「v5 の直近の予定はないが、両方のバージョンを保守する」とコメント |
npm では、2026年9月時点でも next-auth の latest は 4 系、v5 は beta タグのままです。すでに Auth.js で動いているプロジェクトは保守しながら使い続け、新しく作るなら Better Auth を選ぶ、という整理が発表の趣旨に沿っています。 Auth.js のサイトには Better Auth への移行ガイドも公開されています。
検証環境と完成形のファイル構成
| 項目 | バージョン |
|---|---|
| Next.js | 16.3.6(Turbopack) |
| React | 19.2.8 |
| Better Auth | 1.7.6(CLI の auth も 1.7.6) |
| better-sqlite3 | 13.0.3(SQLite 3.53.4) |
| Node.js | 22.22.2 |
| Playwright / Chromium | 1.63.0 / 141.0.7390.37 |
.env
proxy.ts # Cookie の存在チェック(楽観的)
lib/
├── auth.ts # Better Auth の設定
└── session.ts # getSession / requireSession(本検証)
app/
├── api/auth/[...all]/route.ts # 認証 API の入口
├── actions/
│ ├── auth.ts # 登録・ログイン・ログアウト
│ └── profile.ts # 保護された Server Action
├── auth-form.tsx # 入力フォーム(Client Component)
├── signup/page.tsx
├── login/page.tsx
├── dashboard/page.tsx # 保護ページ
└── page.tsx
全体の流れを図にすると次のとおりです。
sequenceDiagram
participant B as ブラウザ
participant P as proxy.ts
participant S as ページ / Server Action
participant A as Better Auth
participant D as SQLite
B->>S: 登録フォーム送信(Server Action)
S->>A: auth.api.signUpEmail
A->>D: user・account・session を保存
A-->>B: Set-Cookie: better-auth.session_token(HttpOnly)
B->>P: GET /dashboard
P->>P: Cookie があるか確認(DB は見ない)
P->>S: 通過
S->>A: auth.api.getSession
A->>D: セッションを照合
S-->>B: ダッシュボードを表示
1. インストールと環境変数
create-next-app@16 で作ったプロジェクトに追加します。
npm i better-auth better-sqlite3 server-only
npm i -D @types/better-sqlite3
# .env(BETTER_AUTH_SECRET は32文字以上のランダムな値)
BETTER_AUTH_SECRET=(openssl rand -base64 32 の出力)
BETTER_AUTH_URL=http://localhost:7222
BETTER_AUTH_SECRET は暗号化やハッシュに使われる値で、公式は「32文字以上・高いエントロピーで生成」するよう求めています。 .env は Git に含めず、本番は環境変数として設定します。BETTER_AUTH_URL はアプリの URL で、本番では https:// の URL にします。
2. Better Auth の設定(lib/auth.ts)
// lib/auth.ts
import { betterAuth } from 'better-auth'
import { nextCookies } from 'better-auth/next-js'
import Database from 'better-sqlite3'
export const auth = betterAuth({
database: new Database('sqlite.db'),
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7日
updateAge: 60 * 60 * 24, // 1日ごとに有効期限を延長
},
plugins: [nextCookies()], // Server Action から Cookie をセットするため(plugins の最後に置く)
})
| 設定 | 意味 |
|---|---|
database |
better-sqlite3 のインスタンスをそのまま渡せる(Node.js 22.5 以降なら node:sqlite も可) |
emailAndPassword.enabled |
メール/パスワード認証を有効化 |
minPasswordLength |
パスワードの最小文字数 |
session.expiresIn / updateAge |
セッションの有効期間と、延長する間隔(秒) |
nextCookies() |
Server Action の中で auth.api を呼んだとき、Set-Cookie を Next.js の cookies() 経由で反映する |
nextCookies() がないと、Server Action からログインしてもブラウザに Cookie が保存されません。Server Action でログインさせるなら必須のプラグインです。
3. テーブルを作る(マイグレーション)
npx auth@latest migrate --yes
実行結果です(auth@1.7.6 で実行)。
ERROR [Better Auth]: Database schema mismatch
Missing tables
user, session, account, verification
help: Run `npx auth migrate` to add the missing tables and columns.
🔑 The migration will affect the following:
-> name, email, emailVerified, image, createdAt, updatedAt fields on user table.
-> expiresAt, token, createdAt, updatedAt, ipAddress, userAgent, userId fields on session table.
-> accountId, providerId, userId, accessToken, refreshToken, idToken, accessTokenExpiresAt, refreshTokenExpiresAt, scope, password, createdAt, updatedAt fields on account table.
-> identifier, value, expiresAt, createdAt, updatedAt fields on verification table.
🚀 migration was completed successfully!
最初の ERROR は「テーブルがまだない」ことを知らせる表示で、その後に作成が完了しています。CLI のパッケージ名は、現在の公式ドキュメントでは auth(npx auth@latest ...)です。 npm の auth パッケージは説明が「The CLI for Better Auth」、リポジトリも better-auth のものでした。
| テーブル | 役割 |
|---|---|
user |
ユーザー(名前・メール・メール確認済みか) |
session |
ログイン中のセッション(トークン・有効期限・IP・User-Agent) |
account |
認証手段ごとの情報。メール/パスワードは providerId = 'credential' でハッシュ化したパスワードを保存 |
verification |
メール確認やパスワード再設定のトークン |
DB を見ると、パスワードは平文ではなくハッシュ化された文字列で保存されていました。
4. 認証 API の入口(Route Handler)
// app/api/auth/[...all]/route.ts
import { auth } from '@/lib/auth'
import { toNextJsHandler } from 'better-auth/next-js'
export const { GET, POST } = toNextJsHandler(auth)
これで /api/auth/sign-up/email、/api/auth/sign-in/email などの API が使えるようになります。curl で直接呼んだ結果です(Origin ヘッダーは後述の CSRF 対策のため付けています)。
$ curl -si -X POST localhost:7222/api/auth/sign-up/email \
-H 'Content-Type: application/json' -H 'Origin: http://localhost:7222' \
-d '{"name":"テスト太郎","email":"curl@example.com","password":"password1234"}'
HTTP/1.1 200 OK
set-cookie: better-auth.session_token=JEUXWDZH...(省略); Max-Age=604800; Path=/; HttpOnly; SameSite=Lax
{"token":"JEUXWDZH...","user":{"name":"テスト太郎","email":"curl@example.com","emailVerified":false,"image":null,"createdAt":"2026-09-29T06:14:57.400Z","updatedAt":"2026-09-29T06:14:57.400Z","id":"avxfNWXZ..."}}
Cookie には HttpOnly(JavaScript から読めない)と SameSite=Lax が付き、Max-Age=604800(7日)は設定した expiresIn と一致しました。 今回は http://localhost での検証のため Secure は付いていません。
エラー時の応答も確認しました。
| 送った内容 | 応答の本文 |
|---|---|
パスワード short(5文字) |
{"message":"Password too short","code":"PASSWORD_TOO_SHORT"} |
| 登録済みのメール | {"message":"User already exists. Use another email.","code":"USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"} |
| 誤ったパスワードでログイン | {"message":"Invalid email or password","code":"INVALID_EMAIL_OR_PASSWORD"} |
Origin: http://evil.example でログイン |
HTTP/1.1 403 FORBIDDEN と {"message":"Invalid origin","code":"INVALID_ORIGIN"} |
ログイン失敗時は「メールかパスワードが違う」とまとめて返し、どちらが違うかを明かしません。 また、許可していない Origin からの POST は 403 で拒否されました。別ドメインのフロントエンドから呼ぶ場合は、Better Auth の trustedOrigins にそのドメインを追加します。
5. セッション取得の共通関数(lib/session.ts)
// lib/session.ts
import 'server-only'
import { cache } from 'react'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { auth } from '@/lib/auth'
// 1回の描画の中では DB への問い合わせを1回にまとめる
export const getSession = cache(async () => {
return auth.api.getSession({ headers: await headers() })
})
// ログイン必須のページ・Server Action の先頭で呼ぶ
export async function requireSession() {
const session = await getSession()
if (!session) redirect('/login')
return session
}
import 'server-only' を書くと、このファイルを Client Component から読み込んだ時点でビルドエラーになり、サーバー専用の処理がブラウザ側に混ざるのを防げます。 getSession は Cookie のトークンを DB のセッションと照合するので、これが「本当の検証」になります。Next.js 公式ガイドの Data Access Layer(DAL)の考え方と同じ形です。
6. 登録・ログイン・ログアウトの Server Action
// app/actions/auth.ts
'use server'
import { redirect } from 'next/navigation'
import { headers } from 'next/headers'
import { APIError } from 'better-auth/api'
import { auth } from '@/lib/auth'
export type FormState = { error?: string; values?: { name?: string; email?: string } } | undefined
export async function signUpAction(_prev: FormState, formData: FormData): Promise<FormState> {
const name = String(formData.get('name') ?? '')
const email = String(formData.get('email') ?? '')
const password = String(formData.get('password') ?? '')
try {
await auth.api.signUpEmail({ body: { name, email, password }, headers: await headers() })
} catch (e) {
// React 19 はアクション後にフォームをリセットするので、入力値を返して再表示する
if (e instanceof APIError) return { error: e.message, values: { name, email } }
throw e
}
redirect('/dashboard') // redirect は try の外で呼ぶ
}
export async function signInAction(_prev: FormState, formData: FormData): Promise<FormState> {
const email = String(formData.get('email') ?? '')
const password = String(formData.get('password') ?? '')
try {
await auth.api.signInEmail({ body: { email, password }, headers: await headers() })
} catch (e) {
if (e instanceof APIError) return { error: e.message, values: { email } }
throw e
}
redirect('/dashboard')
}
export async function signOutAction() {
await auth.api.signOut({ headers: await headers() })
redirect('/login')
}
ポイントは3つです。
redirect()は try の外で呼びます。redirect()は内部で特別な例外を投げて画面遷移させる仕組みなので、catch の中に入ると遷移が止まります。APIErrorだけを catch して画面に返し、想定外のエラーは投げ直します。- エラー時に入力値を返します。 理由は次の「つまずき」で説明します。
検証で見つかったつまずき:エラー後にフォームが空になる
最初は return { error: e.message } だけを返していました。Playwright で「短いパスワードで登録 → エラー表示 → パスワードだけ直して再送信」を試したところ、再送信しても画面が進まず、30秒でタイムアウトしました。 調べると、エラー表示の時点で名前とメールの欄も空になっていました。
React 19 では、<form action> に渡したアクションが終わると、フォームの入力欄が初期値にリセットされます。 名前とメールが空になり、required のブラウザ検証で送信自体が止まっていたのが原因です。入力値を state で返し、defaultValue に入れることで解決しました(パスワードはあえて戻しません)。
7. フォームとページ
// app/auth-form.tsx
'use client'
import { useActionState } from 'react'
import type { FormState } from '@/app/actions/auth'
type Props = {
action: (prev: FormState, formData: FormData) => Promise<FormState>
submitLabel: string
withName?: boolean
}
export function AuthForm({ action, submitLabel, withName }: Props) {
const [state, formAction, pending] = useActionState(action, undefined)
return (
<form action={formAction}>
{withName && (
<label>
名前 <input name="name" defaultValue={state?.values?.name} required />
</label>
)}
<label>
メール <input name="email" type="email" autoComplete="email" defaultValue={state?.values?.email} required />
</label>
<label>
パスワード <input name="password" type="password" autoComplete="current-password" minLength={8} required />
</label>
<button type="submit" disabled={pending}>{submitLabel}</button>
{state?.error && <p role="alert" data-testid="form-error">{state.error}</p>}
</form>
)
}
// app/login/page.tsx(signup も同じ形で、action={signUpAction}・withName を指定)
import Link from 'next/link'
import { AuthForm } from '@/app/auth-form'
import { signInAction } from '@/app/actions/auth'
export default function Page() {
return (
<main>
<h1>ログイン</h1>
<AuthForm action={signInAction} submitLabel="ログイン" />
<Link href="/signup">会員登録はこちら</Link>
</main>
)
}
useActionState の3つ目の戻り値 pending で、送信中はボタンを押せないようにしています。エラーは Better Auth の英語メッセージをそのまま表示していますが、e.body?.code(例:PASSWORD_TOO_SHORT)で日本語の文言に置き換えることもできます。
8. 保護ページ(Server Component)と保護された Server Action
// app/dashboard/page.tsx
import { requireSession } from '@/lib/session'
import { signOutAction } from '@/app/actions/auth'
import { updateNameAction } from '@/app/actions/profile'
export default async function Page() {
const { user, session } = await requireSession() // ページ側で本当の検証をする
return (
<main>
<h1>ダッシュボード</h1>
<p data-testid="welcome">{user.name} さん({user.email})でログイン中</p>
<p>セッション有効期限: {session.expiresAt.toISOString()}</p>
<form action={updateNameAction}>
<input name="name" defaultValue={user.name} />
<button type="submit">名前を変更</button>
</form>
<form action={signOutAction}>
<button type="submit">ログアウト</button>
</form>
</main>
)
}
// app/actions/profile.ts
'use server'
import { revalidatePath } from 'next/cache'
import { headers } from 'next/headers'
import { auth } from '@/lib/auth'
import { requireSession } from '@/lib/session'
// 保護された Server Action:proxy.ts に頼らず、ここでも必ずセッションを確認する
export async function updateNameAction(formData: FormData) {
await requireSession()
const name = String(formData.get('name') ?? '').trim()
if (!name) return
await auth.api.updateUser({ body: { name }, headers: await headers() })
revalidatePath('/dashboard')
}
Server Action は、画面にボタンがなくても外部から POST で呼び出せる「公開 API」と同じ扱いです。 Next.js 公式も、Server Action の中で必ず認証・認可を確認するよう案内しています。ページに requireSession() があっても、Action 側にも書きます。
公式ガイドは、レイアウト(layout.tsx)での認証チェックにも注意を促しています。 レイアウトはページ遷移のたびに再描画されないため、セッションの確認は各ページやデータ取得関数(DAL)で行います。
9. proxy.ts で未ログインを入口で振り分ける
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { getSessionCookie } from 'better-auth/cookies'
// Cookie の「存在」だけを見る楽観的チェック。本当の検証はページ・Server Action 側で行う
export function proxy(request: NextRequest) {
const sessionCookie = getSessionCookie(request)
if (!sessionCookie) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}
Better Auth のドキュメントは、getSessionCookie は Cookie の存在を確認するだけで中身を検証しないこと、保護が必要な処理では必ずサーバー側でセッションを検証することを明記しています。 Next.js 公式の認証ガイドも、Proxy はプリフェッチを含む多くのリクエストで動くため、Cookie からの楽観的チェックにとどめて DB チェックを避けるよう案内しています。
実際に、Cookie の中身が偽物の場合にどうなるかを確かめました。
# Cookie なし → proxy.ts がリダイレクト
$ curl -si localhost:7222/dashboard
HTTP/1.1 307 Temporary Redirect
location: /login
# 偽の Cookie → proxy.ts は通過するが、ページの requireSession() がリダイレクト
$ curl -si -b 'better-auth.session_token=fake' localhost:7222/dashboard
HTTP/1.1 307 Temporary Redirect
location: /login
偽の Cookie は proxy.ts を通過しますが、ページ側の getSession() が DB と照合して弾きました。 proxy.ts だけでは守れず、ページ側の検証があって初めて安全になることが分かります。
| チェックの場所 | 何を見るか | 目的 |
|---|---|---|
| proxy.ts | Cookie があるか | 未ログインの人を早くログイン画面へ。表示の手間を省く |
| ページ(Server Component) | セッションが DB に有効な状態であるか | 保護されたデータを表示する前の本確認 |
| Server Action / Route Handler | 同上+権限 | データを変更する前の本確認 |
Proxy は Node.js ランタイムで動くので、Better Auth の公式ドキュメントには proxy.ts の中で auth.api.getSession() を呼んで完全に検証する例もあります。ただし、すべての対象リクエストで DB 問い合わせが発生するため、本記事では Next.js 公式ガイドの方針に合わせ、Proxy は Cookie の確認だけにしています。
10. Playwright で一連の流れを確認
next build → next start -p 7222 で起動し、Playwright(Chromium)で「未ログインで /dashboard → 短いパスワードで登録 → 正しく登録 → 名前を変更 → ログアウト → /dashboard → 誤ったパスワード → 正しくログイン」を自動実行しました(短いパスワードの送信時は、サーバー側の検証を確かめるため minlength 属性を外しています)。
実行結果です。
1. 未ログインで/dashboard url=/login
2. 短いパスワードで登録 url=/signup alert="Password too short"
3. 正しく登録 url=/dashboard "備品 花子 さん(hanako@example.com)でログイン中"
cookie: better-auth.session_token httpOnly=true sameSite=Lax secure=false expires=2026-10-06T06:17:02.567Z
document.cookie から見えるか: ""
4. Server Actionで名前変更 url=/dashboard "備品 花子(更新) さん(hanako@example.com)でログイン中"
5. ログアウト url=/login cookie残り=0
6. ログアウト後に/dashboard url=/login
7. 誤ったパスワード url=/login alert="Invalid email or password"
8. 正しくログイン url=/dashboard "備品 花子(更新) さん(hanako@example.com)でログイン中"
| 確認したこと | 結果 |
|---|---|
| 未ログインで保護ページ | /login へ移動 |
| 登録直後のログイン状態 | そのまま /dashboard に入れた |
| Cookie の属性 | HttpOnly・SameSite=Lax、有効期限は7日後 |
| JavaScript から Cookie が読めるか | document.cookie は空(読めない) |
| 保護された Server Action | 名前の変更が反映された |
| ログアウト | Cookie が削除され、DB のセッションも削除(ユーザーごとのセッション数で確認) |
| ログアウト後の保護ページ | /login へ移動 |
| 再ログイン | 更新後の名前で表示 |
セキュリティチェックリスト
| 項目 | この記事の実装 | 確認方法 |
|---|---|---|
| シークレットが32文字以上のランダム値 | openssl rand -base64 32 |
.env を Git に入れていないか |
| 本番は HTTPS | BETTER_AUTH_URL を https:// に |
Cookie に Secure が付くか |
| セッション Cookie が HttpOnly | Better Auth の既定 | document.cookie が空 |
| SameSite が設定されている | Lax(既定) |
レスポンスの Set-Cookie |
| 許可していない Origin を拒否 | Better Auth の既定(403) | 別 Origin から POST |
| パスワードはハッシュで保存 | Better Auth の既定 | account.password が平文でない |
| パスワードの最小長 | minPasswordLength: 8 |
短い値でエラー |
| ログイン失敗時に詳細を明かさない | Invalid email or password |
どちらが違うかを出さない |
| ページで本検証 | requireSession() |
偽 Cookie で /login へ |
| Server Action でも本検証 | requireSession() |
Action の先頭にあるか |
| Proxy は楽観的チェックのみ | getSessionCookie() |
DB 問い合わせをしていない |
| サーバー専用コードの混入防止 | import 'server-only' |
Client から import でビルドエラー |
| 返すユーザー情報を最小限に | 名前とメールのみ表示 | パスワード等を props に渡さない |
| ログイン試行の制限 | Better Auth のレート制限(本番で既定有効) | 公式ドキュメントで設定値を確認 |
| メール確認・パスワード再設定 | 本記事の範囲外 | requireEmailVerification などを検討 |
レート制限(短時間の大量ログイン試行を制限する仕組み)は、Better Auth では本番環境で既定で有効になり、開発環境では無効です。 本記事は next start(本番モード)で動かしましたが、ログイン試行の制限値までは検証していないため、導入時は公式ドキュメントで設定を確認してください。
よくある質問
Q. proxy.ts だけでログイン必須にできますか? A. 入口の振り分けはできますが、それだけでは不十分です。今回の検証でも、偽の Cookie は proxy.ts を通過しました。Next.js 公式・Better Auth 公式ともに、ページや Server Action の中でセッションを検証するよう案内しています。
Q. Auth.js(NextAuth.js)はもう使わないほうがよいですか? A. 2025年9月の発表では、既存ユーザーは使い続けてよく、セキュリティと緊急の問題への対応は続くとされています。新しく作るプロジェクトには Better Auth が推奨されており、移行ガイドも公開されています。
Q. SQLite 以外の DB でも同じコードで動きますか?
A. database に渡すものを変えれば、ほかのコードはそのまま使えます。PostgreSQL なら pg の Pool、ORM を使うなら Drizzle や Prisma のアダプターを渡します。テーブルは同じく npx auth@latest migrate(ORM の場合は generate)で用意します。
Q. クライアント側(Client Component)からログインさせたい場合は?
A. better-auth/react の createAuthClient() で作ったクライアントの signIn.email() などを使います。その場合は Route Handler 経由で Cookie が保存されるため nextCookies() は不要ですが、保護ページと Server Action でのセッション検証は同じように必要です。
まとめ
- 認証はライブラリに任せる。新規なら Better Auth、既存の Auth.js は保守しながら使い続けられる
- Better Auth は
betterAuth()の設定・toNextJsHandler・npx auth@latest migrateの3点で動き出す - Server Action でログインさせるなら
nextCookies()プラグインが必須、redirect()は try の外で呼ぶ - React 19 はアクション後にフォームをリセットするので、エラー時は入力値を返して
defaultValueで戻す - proxy.ts は Cookie の存在チェックだけ。偽の Cookie は通過するので、ページと Server Action で
getSession()による本検証を行う - Cookie の HttpOnly・SameSite、Origin の検証、パスワードのハッシュ化は Better Auth の既定で有効なことを実際に確認できた
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Next.jsの認証実装|Better Authでログインを作る完全コード」
株式会社RJCでは、安全なログイン機能のつくり方を、実際に動かして確かめながらチームで共有しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。