Claude Code से सुरक्षित Password Reset लागू करें
Claude Code से सुरक्षित password reset बनाएं: random token, hash storage, rate limit, email, session revoke, MFA separation और audit log।
Password reset कोई छोटी सुविधा नहीं है। यह account recovery का रास्ता है। अगर यह रास्ता login से कमजोर है, तो attacker login तोड़ने की जगह reset flow का इस्तेमाल करेगा। आम गलतियां हैं: यह बता देना कि email registered है या नहीं, raw token database में रखना, link को बहुत देर तक valid रखना, एक ही token को बार-बार चलने देना, reset के बाद पुराने sessions को active रखना, और MFA recovery को password reset के साथ मिला देना।
Claude Code इस workflow को अच्छी तरह बना सकता है, लेकिन prompt साफ होना चाहिए। सिर्फ “forgot password page बना दो” कहना काफी नहीं है। आपको acceptance criteria लिखने होंगे: user existence leak नहीं होगा, random token बनेगा, token hash ही store होगा, expiry छोटी होगी, token one-time होगा, rate limit लगेगा, email template सुरक्षित होगा, Referrer-Policy और noindex होंगे, sessions revoke होंगे, audit log बनेगा, और MFA reset अलग flow रहेगा।
यह guide OWASP के Forgot Password Cheat Sheet, Authentication Cheat Sheet, और Testing for Weak Password Change or Reset Functionalities पर आधारित है। व्यापक सुरक्षा के लिए Claude Code security best practices और Claude Code environment management भी देखें।
सुरक्षित flow कैसा होना चाहिए
Reset token को temporary key समझें। User को यह key email से मिलती है, लेकिन database में original key नहीं रखनी चाहिए। केवल hash रखने से database या backup leak होने पर भी usable reset link नहीं बनता।
flowchart TD
A[User email submit करता है] --> B[हर email के लिए same response]
B --> C{rate limit ok}
C -- no --> Z[Account existence reveal नहीं होती]
C -- yes --> D{User exists}
D -- no --> Z
D -- yes --> E[Node crypto से random token]
E --> F[सिर्फ SHA-256 token hash store]
F --> G[HTTPS reset link email]
G --> H[noindex और Referrer-Policy वाली reset page]
H --> I[User नया password submit करता है]
I --> J[Transaction में token one-time consume]
J --> K[Password hash और sessions revoke]
K --> L[Audit log और फिर से login]
| विषय | निर्णय | कारण |
|---|---|---|
| User existence | Existing और missing email पर same response | Account enumeration रोकना |
| Token | crypto.randomBytes(32).toString("base64url") | Guess करना मुश्किल |
| Storage | केवल SHA-256 hash | DB leak से reset link usable नहीं होगा |
| Expiry | 30 minutes | Risk window छोटा |
| Usage | One-time | Forward, history और log reuse रोकना |
| rate limit | IP और normalized email | Email flood और brute force कम करना |
| Password कभी नहीं भेजना | Email secret vault नहीं है | |
| Reset page | noindex, Referrer-Policy: no-referrer | Search indexing और Referer leak रोकना |
| Reset के बाद | Sessions revoke, auto-login नहीं | Stolen sessions हटाना |
| MFA | इस flow में MFA reset नहीं | MFA recovery अलग और मजबूत flow मांगती है |
Claude Code को किस granularity में काम दें
Security feature को छोटे reviewable tasks में बांटना बेहतर है।
- Prisma schema में
PasswordResetToken,Session,AuditLogजोड़ें। - Node crypto service बनाएं: token, hash, expiry, one-time use, rate limit।
- Next.js App Router में request और confirm API routes बनाएं।
- Email template, reset page,
Referrer-Policy,X-Robots-Tag, cache headers जोड़ें। - Tests लिखें: non-enumeration, expired token, reused token, session revoke।
Claude Code prompt इस तरह दें:
Next.js App Router + TypeScript + Prisma app में password reset implement करें।
Constraints:
- Existing और non-existing emails same JSON और similar response time दें
- Reset token Node crypto से कम से कम 32 bytes का बने और DB में सिर्फ SHA-256 hash store हो
- Token 30 minutes में expire हो और one-time use हो
- Request API में IP और normalized email based rate limit हो
- Reset page पर noindex और Referrer-Policy no-referrer हो
- Successful reset के बाद सभी sessions revoke हों और auto-login न हो
- MFA reset implement न करें; MFA recovery अलग flow रहे
- Raw token, reset URL, password logs में न आएं
सिर्फ auth, Prisma, email और tests से जुड़े जरूरी files बदलें।
Prisma schema
यह minimum schema है। अगर आपके पास पहले से User या Session model है, तो fields merge करें।
// prisma/schema.prisma
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
mfaEnabled Boolean @default(false)
passwordChangedAt DateTime?
createdAt DateTime @default(now())
resetTokens PasswordResetToken[]
sessions Session[]
auditLogs AuditLog[]
}
model PasswordResetToken {
id String @id @default(cuid())
userId String
tokenHash String @unique
expiresAt DateTime
usedAt DateTime?
createdAt DateTime @default(now())
requestedIp String?
requestedUserAgent String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, expiresAt])
@@index([createdAt])
}
model Session {
id String @id @default(cuid())
userId String
revokedAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, revokedAt])
}
model AuditLog {
id String @id @default(cuid())
userId String?
event String
ip String?
userAgent String?
metadata Json?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([userId, createdAt])
@@index([event, createdAt])
}
Crypto, email और session revoke वाला service
यह code छोटे Next.js app में copy-paste करके test किया जा सकता है। @prisma/client, bcryptjs, और zod install करें। Production में Map rate limit को Redis या किसी shared store से बदलें।
// src/lib/password-reset.ts
import crypto from "node:crypto";
import bcrypt from "bcryptjs";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const RESET_TTL_MINUTES = 30;
const TOKEN_BYTES = 32;
const MIN_RESPONSE_MS = 450;
export const REQUEST_RESPONSE = {
message: "If an account exists for that email, a password reset link has been sent.",
};
const memoryLimits = new Map<string, { count: number; resetAt: number }>();
function normalizeEmail(email: string) {
return email.trim().toLowerCase();
}
function hashToken(token: string) {
return crypto.createHash("sha256").update(token, "utf8").digest("hex");
}
function appOrigin() {
const origin = process.env.APP_ORIGIN;
if (!origin || !origin.startsWith("https://")) throw new Error("APP_ORIGIN must be HTTPS");
return origin.replace(/\/$/, "");
}
function isLimited(key: string, max: number, windowMs: number) {
const now = Date.now();
const current = memoryLimits.get(key);
if (!current || current.resetAt <= now) {
memoryLimits.set(key, { count: 1, resetAt: now + windowMs });
return false;
}
current.count += 1;
return current.count > max;
}
async function padResponse(startedAt: number) {
const elapsed = Date.now() - startedAt;
if (elapsed < MIN_RESPONSE_MS) {
await new Promise((resolve) => setTimeout(resolve, MIN_RESPONSE_MS - elapsed));
}
}
async function sendPasswordResetEmail(input: { to: string; resetUrl: string }) {
const html = `
<div style="font-family:Arial,sans-serif;line-height:1.6;color:#111827">
<h1 style="font-size:20px">Reset your password</h1>
<p>This link expires in 30 minutes. If you did not request it, ignore this email.</p>
<p><a href="${input.resetUrl}" style="display:inline-block;background:#2563eb;color:#fff;padding:12px 18px;border-radius:6px;text-decoration:none">Reset password</a></p>
</div>
`;
if (!process.env.RESEND_API_KEY) {
console.info("Password reset email skipped locally", { to: input.to, resetUrl: input.resetUrl });
return;
}
const response = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: process.env.MAIL_FROM ?? "support@example.com",
to: input.to,
subject: "Reset your password",
html,
}),
});
if (!response.ok) throw new Error(`Failed to send reset email: ${response.status}`);
}
export async function requestPasswordReset(input: { email: string; ip?: string; userAgent?: string }) {
const startedAt = Date.now();
const email = normalizeEmail(input.email);
const ip = input.ip ?? "unknown";
const limited =
isLimited(`password-reset:ip:${ip}`, 20, 60 * 60 * 1000) ||
isLimited(`password-reset:email:${email}`, 5, 15 * 60 * 1000);
if (limited) {
await padResponse(startedAt);
return REQUEST_RESPONSE;
}
const user = await prisma.user.findUnique({ where: { email }, select: { id: true, email: true } });
if (user) {
const token = crypto.randomBytes(TOKEN_BYTES).toString("base64url");
const expiresAt = new Date(Date.now() + RESET_TTL_MINUTES * 60 * 1000);
await prisma.passwordResetToken.create({
data: {
userId: user.id,
tokenHash: hashToken(token),
expiresAt,
requestedIp: ip,
requestedUserAgent: input.userAgent?.slice(0, 300),
},
});
await prisma.auditLog.create({
data: {
userId: user.id,
event: "password_reset_requested",
ip,
userAgent: input.userAgent?.slice(0, 300),
metadata: { expiresAt: expiresAt.toISOString() },
},
});
await sendPasswordResetEmail({
to: user.email,
resetUrl: `${appOrigin()}/reset-password?token=${encodeURIComponent(token)}`,
});
}
await padResponse(startedAt);
return REQUEST_RESPONSE;
}
export async function resetPassword(input: { token: string; newPassword: string; ip?: string; userAgent?: string }) {
if (input.newPassword.length < 12 || input.newPassword.length > 128) {
throw new Error("invalid_password");
}
const tokenHash = hashToken(input.token);
const now = new Date();
const passwordHash = await bcrypt.hash(input.newPassword, 12);
return prisma.$transaction(async (tx) => {
const resetToken = await tx.passwordResetToken.findFirst({
where: { tokenHash, expiresAt: { gt: now }, usedAt: null },
select: { id: true, userId: true },
});
if (!resetToken) throw new Error("invalid_or_expired_token");
const claimed = await tx.passwordResetToken.updateMany({
where: { id: resetToken.id, usedAt: null },
data: { usedAt: now },
});
if (claimed.count !== 1) throw new Error("invalid_or_expired_token");
await tx.user.update({
where: { id: resetToken.userId },
data: { passwordHash, passwordChangedAt: now },
});
await tx.session.updateMany({
where: { userId: resetToken.userId, revokedAt: null },
data: { revokedAt: now },
});
await tx.auditLog.create({
data: {
userId: resetToken.userId,
event: "password_reset_completed",
ip: input.ip,
userAgent: input.userAgent?.slice(0, 300),
metadata: { sessionsRevoked: true },
},
});
return { message: "Password updated. Please sign in again." };
});
}
API routes और headers
Request API को हमेशा same body लौटानी चाहिए। Confirm API सिर्फ यह बताए कि link invalid या expired है।
// src/app/api/auth/password-reset/request/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import { REQUEST_RESPONSE, requestPasswordReset } from "@/lib/password-reset";
const bodySchema = z.object({ email: z.string().email().max(320) });
function clientIp(request: Request) {
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
}
export async function POST(request: Request) {
const parsed = bodySchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) return NextResponse.json(REQUEST_RESPONSE, { status: 202 });
await requestPasswordReset({
email: parsed.data.email,
ip: clientIp(request),
userAgent: request.headers.get("user-agent") ?? undefined,
});
return NextResponse.json(REQUEST_RESPONSE, { status: 202 });
}
// src/app/api/auth/password-reset/confirm/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import { resetPassword } from "@/lib/password-reset";
const bodySchema = z
.object({
token: z.string().min(32).max(256),
password: z.string().min(12).max(128),
confirmPassword: z.string().min(12).max(128),
})
.refine((value) => value.password === value.confirmPassword);
export async function POST(request: Request) {
const parsed = bodySchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) {
return NextResponse.json({ message: "The reset link is invalid or the password policy was not met." }, { status: 400 });
}
try {
const result = await resetPassword({
token: parsed.data.token,
newPassword: parsed.data.password,
userAgent: request.headers.get("user-agent") ?? undefined,
});
return NextResponse.json(result, { status: 200 });
} catch {
return NextResponse.json({ message: "The reset link is invalid or expired." }, { status: 400 });
}
}
// next.config.mjs
const nextConfig = {
async headers() {
return [
{
source: "/reset-password",
headers: [
{ key: "Referrer-Policy", value: "no-referrer" },
{ key: "X-Robots-Tag", value: "noindex, nofollow" },
{ key: "Cache-Control", value: "no-store" },
],
},
];
},
};
export default nextConfig;
असली use cases
पहला case SaaS user का है जो password भूल गया। Short expiry, one-time token और session revoke पुराने phone, shared browser या stolen session से risk कम करते हैं।
दूसरा case admin account है। Password reset के बाद भी normal login और MFA होना चाहिए। अगर authenticator खो गया है, तो two-factor authentication guide जैसे अलग MFA recovery workflow की जरूरत है।
तीसरा case support का है। “Email नहीं आया” पूछने पर support audit log में request time, IP, User-Agent और delivery status देखे, लेकिन raw token नहीं देखे।
चौथा case B2B tenant पर reset email flood है। IP और email based rate limit support queue भरने से पहले signal देता है।
गलतियां और review checklist
सबसे खतरनाक गलती है यह email registered नहीं है लौटाना। यह account enumeration endpoint बन जाता है। Response time का फर्क भी यही leak कर सकता है।
दूसरी गलती raw token store करना है। अगर token DB, logs, monitoring या support tool में दिखता है, तो read access account takeover access बन सकता है।
तीसरी गलती non-expiring या reusable token है। Email forward हो सकता है, browser history sync हो सकती है, screenshot share हो सकता है। usedAt transaction में set होना चाहिए।
Review में यह जांचें: same response, raw token/password log नहीं, DB में सिर्फ tokenHash, expiresAt और usedAt, session revoke, auto-login नहीं, reset page headers, MFA अलग, audit logs present।
npm install @prisma/client bcryptjs zod
npx prisma migrate dev --name add_password_reset
npm run lint
npm test
निष्कर्ष
Password reset एक छोटा authentication system है। Claude Code से इसे बनाते समय security को acceptance criteria बनाएं: no enumeration, random token, hash storage, short expiry, one-time use, rate limit, safe email, noindex, Referrer-Policy, session revoke, audit log और MFA separation।
ClaudeCodeLab Claude Code के साथ authentication review, training और implementation support कर सकता है। Current login flow, session model, email provider और MFA policy लेकर आएं; इससे पता चल जाता है कि क्या generate किया जा सकता है और कहां human review जरूरी है।
इस article को practically test करते समय चार बातें पहले जांचें: real और fake email same response दें, DB में raw token न हो, expired और used token fail हों, और reset के बाद पुराने sessions protected API access न कर सकें। Masa भी implementation support में पहले यही manual checks करता है, फिर उन्हें automated tests में बदलता है।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code Obsidian to CLAUDE.md workflow: context बार-बार न समझाएं
Obsidian notes को CLAUDE.md operating notes में बदलकर Claude Code sessions को resume करना आसान बनाएं.
Claude Code Revenue CTA Routing: article से PDF, Gumroad और consultation तक
Reader intent के आधार पर free PDF, Gumroad products और consultation तक CTA route करने वाला workflow.
Claude Code टीम हैंडऑफ नियम: review proof, permissions, rollback और revenue path
Claude Code टीम काम के लिए evidence, permission rules, rollback, free PDF, Gumroad और consultation path वाला handoff.