認証
認証を理解することは、アプリケーションのデータを保護するために非常に重要です。このページでは、認証を実装するためにReactとNext.jsのどの機能を使用すべきかについて説明します。
始める前に、以下の3つの概念にプロセスを分解するとよいでしょう:
- 認証:ユーザーが主張する人物であることを確認することを指します。これは、ユーザーがユーザー名やパスワードなどを使用して自分の身元を証明することを要求します。
- セッション管理:リクエスト間でユーザーの認証状態を追跡します。
- 認可:ユーザーがどのルートやデータにアクセスできるかを決定します。
この図は、ReactとNext.jsの機能を使用した認証フローを示しています:
このページの例は、教育目的で基本的なユーザー名とパスワードの認証を扱います。カスタム認証ソリューションを実装するこ ともできますが、セキュリティと単純化の向上のため、認証ライブラリを使用することをお勧めします。これらは認証、セッション管理、認可に関する組み込みのソリューションを提供し、さらにソーシャルログイン、多要素認証、役割ベースのアクセス制御などの追加機能も提供します。Auth Librariesセクションでリストを見つけることができます。
認証
サインアップとログイン機能
ReactのServer ActionsとuseFormState
を使用して、ユーザーのクレデンシャルを取得し、フォームフィールドを検証し、認証プロバイダーのAPIまたはデータベースを呼び出すために<form>
要素を使用できます。
Server Actionsは常にサーバーで実行されるため、認証ロジックを処理するための安全な環境を提供します。
サインアップ/ログイン機能を実装する手順は以下の通りです:
1. ユーザークレデンシャルを取得
ユーザーのクレデンシャルを取得するには、Server Actionが送信時に呼び出されるフォームを作成します。たとえば、ユーザーの名前、メールアドレス、およびパスワードを受け取るサインアップフォームがあります:
- TypeScript
- JavaScript
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
- TypeScript
- JavaScript
export async function signup(formData: FormData) {}
export async function signup(formData) {}
2. サーバーでフォームフィールドを検証
サーバー上でフォームフィールドを検証するために、Server Actionを使用します。オーセンティケーションプロバイダーがフォームの検証を提供しない場合は、ZodやYupのようなスキーマ検証ライブラリを使用することができます。
Zodを例に取ると、適切なエラーメッセージを含むフォームスキーマを定義できます:
- TypeScript
- JavaScript
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})
export type FormState =
| {
errors?: {
name?: string[]
email?: string[]
password?: string[]
}
message?: string
}
| undefined
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})
認証プロバイダーのAPIまたはデータベースへの不要な呼び出しを避けるために、定義されたスキーマに一致しないフォームフィールドがある場合、Server Actionで早期にreturn
することができます。
- TypeScript
- JavaScript
import { SignupFormSchema, FormState } from '@/app/lib/definitions'
export async function signup(state: FormState, formData: FormData) {
// フォームフィールドを検証する
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// フォームフィールドが無効な場合は早期にreturn
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// プロバイダーまたはdbを呼び出してユーザーを作成します...
}
import { SignupFormSchema } from '@/app/lib/definitions'
export async function signup(state, formData) {
// フォームフィールドを検証する
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// フォームフィールドが無効な場合は早期にreturn
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// プロバイダーまたはdbを呼び出してユーザーを作成します...
}
戻って<SignupForm />
で、ReactのuseFormState
フックを使用して、フォーム送信中に検証エラーを表示できます:
- TypeScript
- JavaScript
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { signup } from '@/app/actions/auth'
export function SignupForm() {
const [state, action] = useFormState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="Email" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<SubmitButton />
</form>
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { signup } from '@/app/actions/auth'
export function SignupForm() {
const [state, action] = useFormState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="John Doe" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="john@example.com" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<SubmitButton />
</form>
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
Good to know:
- これらの例では、Next.js App RouterにバンドルされているReactの
useFormState
フックを使用しています。React 19を使用している場合は、代わりにuseActionState
を使用してください。詳細についてはReactのドキュメントを参照してください。- React 19では、
useFormStatus
は返されるオブジェクトにデータ、メソッド、アクションなどの追加キーを含んでいます。React 19を使用していない場合は、pending
キーのみを使用できます。- React 19では、
useActionState
にも返される状態にpending
キーが含まれます。- データを変更する前に、ユーザーがそのアクションを実行する許可を持っていることを常に確認する必要があります。認証と認可を参照してください。
3. ユーザーを作成またはユーザー資格情報を確認
フォームフィールドを検証した後、認証プロバイダーのAPIまたはデータベースを呼び出して新しいユーザーアカウントを作成するか、ユーザーが存在するかどうかを確認することができます。
前の例から続けます:
- TypeScript
- JavaScript
export async function signup(state: FormState, formData: FormData) {
// 1. フォームフィールドを検証します
// ...
// 2. データベースへの挿入のためのデータを準備する
const { name, email, password } = validatedFields.data
// 例として、ユーザーのパスワードを保存する前にハッシュ化する
const hashedPassword = await bcrypt.hash(password, 10)
// 3. ユーザーをデータベースに挿入するか、オーセンティケーションライブラリのAPIを呼び出す
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: 'アカウントの作成中にエラーが発生しました。',
}
}
// TODO:
// 4. ユーザーセッションを作成する
// 5. ユーザーをリダイレクトする
}
export async function signup(state, formData) {
// 1. フォームフィールドを検証します
// ...
// 2. データベースへの挿入のためのデータを準備する
const { name, email, password } = validatedFields.data
// 例として、ユーザーのパスワードを保存する前にハッシュ化する
const hashedPassword = await bcrypt.hash(password, 10)
// 3. ユーザーをデータベースに挿入するか、ライブラリAPIを呼び出す
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: 'アカウントの作成中にエラーが発生しました。',
}
}
// TODO:
// 4. ユーザーセッションを作成する
// 5. ユーザーをリダイレクトする
}
ユーザーアカウントの作成に成功したり、ユーザー資格情報を確認した後、ユーザーの認証状態を管理するためにセッションを作成することができます。セッション管理方法によっては、セッションはcookieまたはデータベース、またはその両方に保存されることがあります。セッション管理セクションに続いてさらに学びましょう。
Tips:
- 上記の例は、教育目的で認証ステップを細かく説明しているため冗長ですが、独自の安全なソリューションを実装することがどれほど複雑になるかを示しています。認証ライブラリを使用してプロセスを簡単にすることをお勧めします。
- ユーザーエクスペリエンスを向上させるため、重複するメールやユーザー名が登録フローの早い段階でないか確認したいかもしれません。たとえば、ユーザーがユーザー名を入力中または入力フィールドがフォーカスを失う際に。これにより不要なフォーム送信を防ぎ、ユーザーに即座のフィードバックを提供できます。use-debounceなどのライブラリを使用して、これらのチェックの頻度を管理できます。
セッション管理
セッション管理は、ユーザーの認証された状態をリクエスト間で保存することを保証します。セッションやトークンの作成、保存、更新、および削除を含みます。
セッションには2種類あります:
- ステートレス: セッションデータ(またはトークン)はブラウザのcookieに保 存されます。このcookieは各リクエストと共に送信され、サーバー上でセッションを確認できます。この方法は単純ですが、正しく実装しないとセキュリティが低下する可能性があります。
- データベース: セッションデータはデータベースに保存され、ユーザーのブラウザには暗号化されたセッションIDのみが受け渡されます。この方法はより安全ですが、複雑でありサーバーリソースを多く使用する可能性があります。
Good to know: いずれの方法も使用できますが、iron-sessionやJoseのようなセッション管理ライブラリを使用することをお勧めします。
ステートレスセッション
ステートレスセッションを作成して管理するには、以下のステップに従う必要があります:
- セッションを署名するために使用される秘密鍵を生成し、それを環境変数として保存します。
- セッション管理ライブラリを使用してセッションデータの暗号化/復号化のロジックを作成します。
- Next.jsの
cookies
APIを使用してcookieを管理します。
上記に加えて、ユーザーがアプリケーションに 戻ってきたときにセッションを更新または更新する機能を追加し、ユーザーがログアウトしたときにセッションを削除することを検討してください。
Good to know: 認証ライブラリがセッション管理を含んでいるかどうか確認してください。
1. 秘密鍵の生成
セッションを署名するための秘密鍵を生成する方法はいくつかあります。たとえば、ターミナルでopenssl
コマンドを使用することができます:
openssl rand -base64 32
このコマンドは、環境変数ファイルで秘密鍵として使用できる、32文字のランダムな文字列を生成します:
SESSION_SECRET=your_secret_key
その後、セッション管理ロジック内でこの鍵を参照できます:
const secretKey = process.env.SESSION_SECRET
2. セッションの暗号化と復号化
次に、好みのセッション管理ライブラリを使用してセッションを暗号化および復号化できます。前の例から続けて、Jose(Edge Runtimeと互換性があります)とReactのserver-only
パッケージを使用して、セッション管理ロジックがサーバーでのみ実行されるようにする例を示します:
- TypeScript
- JavaScript
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload: SessionPayload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session: string | undefined = '') {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('Failed to verify session')
}
}
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session) {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('Failed to verify session')
}
}
Tips:
- ペイロードには、今後のリクエストで使用される、最小限の一意のユーザーデータ(ユーザーのIDやロールなど)が含まれるべきであり、電話番号、メールアドレス、クレジットカード情報などの個人を特定できる情報や、パスワードなどの機密データを含んではいけません。