Implementing Password Reset: Claude Code 활용 가이드
implementing password reset: Claude Code 활용. 실용적인 코드 예시를 포함합니다.
비밀번호리셋機能を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
Claude Code로 리팩토링을 자동화하는 방법
Claude Code를 활용해 코드 리팩토링을 효율적으로 자동화하는 방법을 알아봅니다. 실전 프롬프트와 구체적인 리팩토링 패턴을 소개합니다.
Use Cases
Use Cases
Claude Code로 사이드 프로젝트 개발 속도를 극대화하는 방법 [예제 포함]
Claude Code를 활용해 개인 프로젝트 개발 속도를 획기적으로 높이는 방법을 알아봅니다. 실전 예제와 아이디어부터 배포까지의 워크플로를 포함합니다.
Use Cases
Use Cases
Complete CORS Configuration Guide: Claude Code 활용 가이드
complete cors configuration guide: Claude Code 활용. 실용적인 팁과 코드 예시를 포함합니다.