Réinitialisation de mot de passe avec Claude Code
Découvrez réinitialisation de mot de passe avec Claude Code. Conseils pratiques et exemples de code inclus.
パスワードリセット機能を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)がシンプルでおすすめです。
Related Posts
Comment booster vos projets personnels avec Claude Code [Avec exemples]
Apprenez à accélérer considérablement vos projets de développement personnels avec Claude Code. Inclut des exemples concrets et un workflow pratique de l'idée au déploiement.
Comment automatiser le refactoring avec Claude Code
Apprenez à automatiser efficacement le refactoring de code avec Claude Code. Inclut des prompts pratiques et des patterns de refactoring concrets pour des projets réels.
Guide complet de configuration CORS avec Claude Code
Découvrez le guide complet de configuration CORS avec Claude Code. Conseils pratiques et exemples de code inclus.