Implementing Password Reset with Claude Code
Learn about implementing password reset using Claude Code. Includes practical code examples.
パスワードリセット機能をClaude Codeで実装する
パスワードリセットはほぼすべてのWebアプリに必要な機能ですが、セキュリティ上の落とし穴も多い領域です。Claude Codeを使えば、トークンの安全な生成・検証、レート制限、メール送信を含む堅牢な実装を構築できます。
リセットフローの全体像
- ユーザーがメールアドレスを入力
- サーバーがリセットトークンを生成しメール送信
- ユーザーがメール内のリンクをクリック
- 新しいパスワードを入力して更新
> セキュアなパスワードリセット機能を実装して。
> トークンの有効期限は1時間、使用は1回のみ。
> レート制限とメール送信も含めて。
トークン生成とメール送信
// src/services/password-reset.ts
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { prisma } from '@/lib/prisma';
import { sendEmail } from '@/lib/email';
export class PasswordResetService {
private static TOKEN_EXPIRY_HOURS = 1;
async requestReset(email: string) {
const user = await prisma.user.findUnique({ where: { email } });
// ユーザーが存在しなくても同じレスポンスを返す(情報漏洩防止)
if (!user) return { success: true };
// レート制限: 同一メールへの送信は5分に1回まで
const recentRequest = await prisma.passwordReset.findFirst({
where: {
userId: user.id,
createdAt: { gt: new Date(Date.now() - 5 * 60 * 1000) },
},
});
if (recentRequest) return { success: true };
// セキュアなトークン生成
const token = crypto.randomBytes(32).toString('hex');
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
// 古いトークンをDelete
await prisma.passwordReset.deleteMany({ where: { userId: user.id } });
// 新しいトークンを保存
await prisma.passwordReset.create({
data: {
userId: user.id,
token: hashedToken,
expiresAt: new Date(
Date.now() + this.constructor.TOKEN_EXPIRY_HOURS * 60 * 60 * 1000
),
},
});
// リセットメール送信
const resetUrl = `${process.env.APP_URL}/reset-password?token=${token}`;
await sendEmail({
to: email,
subject: 'パスワードリセットのご案内',
html: `
<h2>パスワードリセット</h2>
<p>以下のリンクをクリックしてパスワードをリセットしてください。</p>
<a href="${resetUrl}" style="display:inline-block;background:#3B82F6;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;">
パスワードをリセット
</a>
<p style="color:#666;font-size:14px;margin-top:16px;">
このリンクは1時間で有効期限が切れます。<br />
心当たりがない場合は、このメールを無視してください。
</p>
`,
});
return { success: true };
}
async resetPassword(token: string, newPassword: string) {
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
const resetRecord = await prisma.passwordReset.findFirst({
where: {
token: hashedToken,
expiresAt: { gt: new Date() },
used: false,
},
});
if (!resetRecord) {
throw new Error('無効または期限切れのトークンです');
}
// パスワードのバリデーション
if (newPassword.length < 8) {
throw new Error('パスワードは8文字以上である必要があります');
}
const hashedPassword = await bcrypt.hash(newPassword, 12);
// トランザクションでパスワード更新とトークン無効化
await prisma.$transaction([
prisma.user.update({
where: { id: resetRecord.userId },
data: { password: hashedPassword },
}),
prisma.passwordReset.update({
where: { id: resetRecord.id },
data: { used: true },
}),
]);
return { success: true };
}
}
リセットフォームのUI
// src/app/(auth)/reset-password/page.tsx
'use client';
import { useState } from 'react';
import { useSearchParams } from 'next/navigation';
export default function ResetPasswordPage() {
const searchParams = useSearchParams();
const token = searchParams.get('token');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirmPassword) {
setError('パスワードが一致しません');
return;
}
try {
const res = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, password }),
});
if (!res.ok) throw new Error((await res.json()).error);
setStatus('success');
} catch (err: any) {
setError(err.message);
setStatus('error');
}
};
if (status === 'success') {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">パスワードを更新しました</h2>
<a href="/login" className="text-blue-600 hover:underline">ログインページへ</a>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<form onSubmit={handleSubmit} className="bg-white p-8 rounded-xl shadow-lg w-full max-w-md">
<h1 className="text-2xl font-bold mb-6">新しいパスワード</h1>
{error && <p className="text-red-500 text-sm mb-4">{error}</p>}
<div className="space-y-4">
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="新しいパスワード(8文字以上)"
className="w-full border rounded-lg px-4 py-3"
minLength={8}
required
/>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="パスワードの確認"
className="w-full border rounded-lg px-4 py-3"
required
/>
<button type="submit" className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium">
パスワードを更新
</button>
</div>
</form>
</div>
);
}
セキュリティチェックリスト
- トークンは暗号学的に安全な乱数で生成する
- DBにはハッシュ化したトークンを保存する
- トークンの有効期限を設ける(1時間程度)
- トークンは1回使用後に無効化する
- ユーザーの存在有無を応答で漏らさない
関連記事
認証全般の設計は認証機能の実装、二要素認証との組み合わせは二要素認証の実装をご覧ください。
メール送信にはResend(resend.com)がシンプルでおすすめです。
#Claude Code
#password reset
#authentication
#security
#email
Related Posts
Use Cases
Use Cases
How to Supercharge Your Side Projects with Claude Code [With Examples]
How to Supercharge Your Side Projects with Claude Code [With Examples]. A practical guide with code examples.
Use Cases
Use Cases
How to Automate Refactoring with Claude Code
Learn how to automate refactoring using Claude Code. Includes practical code examples and step-by-step guidance.
Use Cases
Use Cases
Complete CORS Configuration Guide with Claude Code
Learn about complete cors configuration guide using Claude Code. Practical tips and code examples included.