[{"data":1,"prerenderedAt":388},["ShallowReactive",2],{"blog-best-practices/password":3},{"id":4,"title":5,"body":6,"category":364,"date":365,"dateModified":365,"description":366,"draft":367,"extension":368,"faq":369,"featured":367,"headerVariant":373,"image":374,"keywords":374,"meta":375,"navigation":376,"ogDescription":377,"ogTitle":374,"path":378,"readTime":379,"schemaOrg":380,"schemaType":381,"seo":382,"sitemap":383,"stem":384,"tags":385,"twitterCard":386,"__hash__":387},"blog/blog/best-practices/password.md","Password Security Best Practices: Hashing, Storage, and Policies",{"type":7,"value":8,"toc":354},"minimark",[9,20,29,34,37,124,139,148,152,155,180,189,193,196,205,209,212,221,225,228,242,251,274,296,300,303,323,342],[10,11,12],"tldr",{},[13,14,15,19],"p",{},[16,17,18],"strong",{},"The #1 password security best practice is using bcrypt or argon2id for password hashing instead of SHA-256 or MD5."," Require minimum 8 characters without complexity rules. Check against breached password databases. Implement rate limiting and account lockout. Offer passwordless options and 2FA.",[21,22,23],"quotable-box",{},[24,25,26],"blockquote",{},[13,27,28],{},"\"The strength of your password storage determines whether a data breach becomes a minor incident or a catastrophic compromise of every user account.\"",[30,31,33],"h2",{"id":32},"best-practice-1-use-the-right-hashing-algorithm-3-min","Best Practice 1: Use the Right Hashing Algorithm 3 min",[13,35,36],{},"Only use password-specific hashing algorithms:",[38,39,40,56],"table",{},[41,42,43],"thead",{},[44,45,46,50,53],"tr",{},[47,48,49],"th",{},"Algorithm",[47,51,52],{},"Status",[47,54,55],{},"Use Case",[57,58,59,71,82,93,104,115],"tbody",{},[44,60,61,65,68],{},[62,63,64],"td",{},"argon2id",[62,66,67],{},"Best choice",[62,69,70],{},"New applications",[44,72,73,76,79],{},[62,74,75],{},"bcrypt",[62,77,78],{},"Excellent",[62,80,81],{},"Widely supported",[44,83,84,87,90],{},[62,85,86],{},"scrypt",[62,88,89],{},"Good",[62,91,92],{},"Alternative to bcrypt",[44,94,95,98,101],{},[62,96,97],{},"PBKDF2",[62,99,100],{},"Acceptable",[62,102,103],{},"Compliance requirements",[44,105,106,109,112],{},[62,107,108],{},"SHA-256",[62,110,111],{},"NEVER",[62,113,114],{},"Not for passwords",[44,116,117,120,122],{},[62,118,119],{},"MD5",[62,121,111],{},[62,123,114],{},[125,126,128],"code-block",{"label":127},"Password hashing with bcrypt",[129,130,135],"pre",{"className":131,"code":133,"language":134},[132],"language-text","import bcrypt from 'bcrypt';\n\nconst SALT_ROUNDS = 12;  // Adjust based on server capacity\n\nasync function hashPassword(password) {\n  return bcrypt.hash(password, SALT_ROUNDS);\n}\n\nasync function verifyPassword(password, hash) {\n  return bcrypt.compare(password, hash);\n}\n\n// Usage\nconst hash = await hashPassword('userPassword123');\n// Stored: $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.G...\n","text",[136,137,133],"code",{"__ignoreMap":138},"",[125,140,142],{"label":141},"Password hashing with argon2",[129,143,146],{"className":144,"code":145,"language":134},[132],"import argon2 from 'argon2';\n\nasync function hashPassword(password) {\n  return argon2.hash(password, {\n    type: argon2.argon2id,  // Recommended variant\n    memoryCost: 65536,      // 64 MB\n    timeCost: 3,            // 3 iterations\n    parallelism: 4,         // 4 threads\n  });\n}\n\nasync function verifyPassword(password, hash) {\n  return argon2.verify(hash, password);\n}\n",[136,147,145],{"__ignoreMap":138},[30,149,151],{"id":150},"best-practice-2-modern-password-policies-2-min","Best Practice 2: Modern Password Policies 2 min",[13,153,154],{},"NIST guidelines recommend simpler, more effective policies:",[156,157,158,162,165,168,171,174,177],"ul",{},[159,160,161],"li",{},"Minimum 8 characters (12+ recommended)",[159,163,164],{},"Maximum 64+ characters (do not limit)",[159,166,167],{},"Allow all characters including spaces",[159,169,170],{},"No complexity requirements (uppercase, symbols)",[159,172,173],{},"No periodic password rotation",[159,175,176],{},"Check against breached password lists",[159,178,179],{},"Show password strength meter",[125,181,183],{"label":182},"Password validation",[129,184,187],{"className":185,"code":186,"language":134},[132],"import { z } from 'zod';\n\nconst passwordSchema = z.string()\n  .min(8, 'Password must be at least 8 characters')\n  .max(128, 'Password too long')\n  .refine(\n    (password) => !isBreachedPassword(password),\n    'This password has appeared in a data breach'\n  );\n\n// Check against Have I Been Pwned\nasync function isBreachedPassword(password) {\n  const hash = crypto\n    .createHash('sha1')\n    .update(password)\n    .digest('hex')\n    .toUpperCase();\n\n  const prefix = hash.slice(0, 5);\n  const suffix = hash.slice(5);\n\n  const response = await fetch(\n    `https://api.pwnedpasswords.com/range/${prefix}`\n  );\n  const text = await response.text();\n\n  return text.includes(suffix);\n}\n",[136,188,186],{"__ignoreMap":138},[30,190,192],{"id":191},"best-practice-3-secure-password-reset-5-min","Best Practice 3: Secure Password Reset 5 min",[13,194,195],{},"Password reset is a common attack vector:",[125,197,199],{"label":198},"Secure password reset flow",[129,200,203],{"className":201,"code":202,"language":134},[132],"import crypto from 'crypto';\n\nasync function requestPasswordReset(email) {\n  const user = await findUserByEmail(email);\n\n  // Always return success (prevent user enumeration)\n  if (!user) {\n    return { message: 'If the email exists, a reset link was sent' };\n  }\n\n  // Generate secure token\n  const token = crypto.randomBytes(32).toString('hex');\n  const hashedToken = crypto\n    .createHash('sha256')\n    .update(token)\n    .digest('hex');\n\n  // Store hashed token with expiry\n  await db.passwordReset.create({\n    userId: user.id,\n    tokenHash: hashedToken,\n    expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour\n  });\n\n  // Send email with plain token\n  await sendEmail(email, {\n    subject: 'Password Reset',\n    link: `https://app.example.com/reset?token=${token}`,\n  });\n\n  return { message: 'If the email exists, a reset link was sent' };\n}\n\nasync function resetPassword(token, newPassword) {\n  const hashedToken = crypto\n    .createHash('sha256')\n    .update(token)\n    .digest('hex');\n\n  const reset = await db.passwordReset.findFirst({\n    where: {\n      tokenHash: hashedToken,\n      expiresAt: { gt: new Date() },\n      used: false,\n    },\n  });\n\n  if (!reset) {\n    throw new Error('Invalid or expired reset link');\n  }\n\n  // Hash and save new password\n  const hash = await hashPassword(newPassword);\n  await db.user.update({\n    where: { id: reset.userId },\n    data: { passwordHash: hash },\n  });\n\n  // Mark token as used and invalidate sessions\n  await db.passwordReset.update({\n    where: { id: reset.id },\n    data: { used: true },\n  });\n\n  await invalidateAllSessions(reset.userId);\n}\n",[136,204,202],{"__ignoreMap":138},[30,206,208],{"id":207},"best-practice-4-rate-limiting-login-attempts-3-min","Best Practice 4: Rate Limiting Login Attempts 3 min",[13,210,211],{},"Protect against brute force attacks:",[125,213,215],{"label":214},"Login rate limiting",[129,216,219],{"className":217,"code":218,"language":134},[132],"import rateLimit from 'express-rate-limit';\n\n// Global rate limit by IP\nconst loginLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,  // 15 minutes\n  max: 5,                     // 5 attempts per window\n  message: 'Too many login attempts, please try again later',\n  standardHeaders: true,\n  legacyHeaders: false,\n});\n\n// Per-account lockout\nconst LOCKOUT_THRESHOLD = 5;\nconst LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes\n\nasync function attemptLogin(email, password, ip) {\n  const user = await findUserByEmail(email);\n\n  if (!user) {\n    // Prevent timing attacks\n    await bcrypt.hash(password, 12);\n    throw new AuthError('Invalid credentials');\n  }\n\n  // Check if account is locked\n  if (user.lockedUntil && user.lockedUntil > new Date()) {\n    throw new AuthError('Account temporarily locked');\n  }\n\n  const valid = await verifyPassword(password, user.passwordHash);\n\n  if (!valid) {\n    // Increment failed attempts\n    const attempts = user.failedAttempts + 1;\n\n    await db.user.update({\n      where: { id: user.id },\n      data: {\n        failedAttempts: attempts,\n        lockedUntil: attempts >= LOCKOUT_THRESHOLD\n          ? new Date(Date.now() + LOCKOUT_DURATION)\n          : null,\n      },\n    });\n\n    throw new AuthError('Invalid credentials');\n  }\n\n  // Reset failed attempts on success\n  await db.user.update({\n    where: { id: user.id },\n    data: { failedAttempts: 0, lockedUntil: null },\n  });\n\n  return createSession(user);\n}\n",[136,220,218],{"__ignoreMap":138},[30,222,224],{"id":223},"best-practice-5-offer-stronger-authentication-10-min","Best Practice 5: Offer Stronger Authentication 10 min",[13,226,227],{},"Passwords alone are not enough:",[156,229,230,233,236,239],{},[159,231,232],{},"Offer two-factor authentication (TOTP, WebAuthn)",[159,234,235],{},"Support passwordless login (magic links, passkeys)",[159,237,238],{},"Encourage password managers",[159,240,241],{},"Implement step-up authentication for sensitive actions",[125,243,245],{"label":244},"TOTP 2FA implementation",[129,246,249],{"className":247,"code":248,"language":134},[132],"import speakeasy from 'speakeasy';\nimport QRCode from 'qrcode';\n\n// Generate 2FA secret\nfunction generate2FASecret(email) {\n  const secret = speakeasy.generateSecret({\n    name: `YourApp (${email})`,\n    issuer: 'YourApp',\n  });\n\n  return {\n    secret: secret.base32,\n    qrCodeUrl: secret.otpauth_url,\n  };\n}\n\n// Verify 2FA token\nfunction verify2FAToken(secret, token) {\n  return speakeasy.totp.verify({\n    secret: secret,\n    encoding: 'base32',\n    token: token,\n    window: 1,  // Allow 1 step before/after\n  });\n}\n\n// Login with 2FA\nasync function loginWith2FA(email, password, totpToken) {\n  const user = await authenticatePassword(email, password);\n\n  if (user.twoFactorEnabled) {\n    if (!totpToken) {\n      return { requiresTwoFactor: true };\n    }\n\n    if (!verify2FAToken(user.twoFactorSecret, totpToken)) {\n      throw new AuthError('Invalid 2FA code');\n    }\n  }\n\n  return createSession(user);\n}\n",[136,250,248],{"__ignoreMap":138},[252,253,254],"info-box",{},[13,255,256,259,260,267,268,273],{},[16,257,258],{},"Official Resources:"," For comprehensive password security guidance, see ",[261,262,266],"a",{"href":263,"rel":264},"https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html",[265],"nofollow","OWASP Password Storage Cheat Sheet"," and ",[261,269,272],{"href":270,"rel":271},"https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html",[265],"OWASP Authentication Cheat Sheet",".",[275,276,277,284,290],"faq-section",{},[278,279,281],"faq-item",{"question":280},"Should I require password changes every 90 days?",[13,282,283],{},"No. NIST no longer recommends periodic password rotation. It leads to weaker passwords (Password1, Password2...). Only require changes when there is evidence of compromise.",[278,285,287],{"question":286},"What about complexity requirements?",[13,288,289],{},"Studies show complexity requirements (uppercase, number, symbol) do not improve security and frustrate users. Focus on length and breach checking instead.",[278,291,293],{"question":292},"How many bcrypt rounds should I use?",[13,294,295],{},"Use the highest value that keeps login under 250ms on your server. Start with 10-12 and benchmark. Increase over time as hardware improves.",[30,297,299],{"id":298},"further-reading","Further Reading",[13,301,302],{},"Put these practices into action with our step-by-step guides.",[156,304,305,311,317],{},[159,306,307],{},[261,308,310],{"href":309},"/blog/how-to/add-security-headers","Add security headers to your app",[159,312,313],{},[261,314,316],{"href":315},"/blog/checklists/pre-deployment-security-checklist","Pre-deployment security checklist",[159,318,319],{},[261,320,322],{"href":321},"/blog/getting-started/first-scan","Run your first security scan",[324,325,326,332,337],"related-articles",{},[327,328],"related-card",{"description":329,"href":330,"title":331},"Complete auth security","/blog/best-practices/authentication","Authentication",[327,333],{"description":334,"href":335,"title":336},"Secure session handling","/blog/best-practices/session","Session Management",[327,338],{"description":339,"href":340,"title":341},"Token-based auth","/blog/best-practices/jwt","JWT Security",[343,344,347,351],"cta-box",{"href":345,"label":346},"/","Start Free Scan",[30,348,350],{"id":349},"check-your-password-security","Check Your Password Security",[13,352,353],{},"Scan for weak password hashing and policy issues.",{"title":138,"searchDepth":355,"depth":355,"links":356},2,[357,358,359,360,361,362,363],{"id":32,"depth":355,"text":33},{"id":150,"depth":355,"text":151},{"id":191,"depth":355,"text":192},{"id":207,"depth":355,"text":208},{"id":223,"depth":355,"text":224},{"id":298,"depth":355,"text":299},{"id":349,"depth":355,"text":350},"best-practices","2026-01-30","Password security best practices. Learn proper password hashing with bcrypt/argon2, secure storage, password policies, and breach detection.",false,"md",[370,371,372],{"question":280,"answer":283},{"question":286,"answer":289},{"question":292,"answer":295},"vibe-green",null,{},true,"Implement secure password handling with modern hashing and storage.","/blog/best-practices/password","11 min read","[object Object]","Article",{"title":5,"description":366},{"loc":378},"blog/best-practices/password",[],"summary_large_image","gQ2OO2rRM5J5naavr6XqxD-s8YNvon64pBbaw3z2t90",1775843925198]