[{"data":1,"prerenderedAt":350},["ShallowReactive",2],{"blog-how-to/hash-passwords-securely":3},{"id":4,"title":5,"body":6,"category":331,"date":332,"dateModified":332,"description":333,"draft":334,"extension":335,"faq":336,"featured":334,"headerVariant":337,"image":336,"keywords":336,"meta":338,"navigation":339,"ogDescription":340,"ogTitle":336,"path":341,"readTime":336,"schemaOrg":342,"schemaType":343,"seo":344,"sitemap":345,"stem":346,"tags":347,"twitterCard":348,"__hash__":349},"blog/blog/how-to/hash-passwords-securely.md","How to Hash Passwords Securely",{"type":7,"value":8,"toc":309},"minimark",[9,13,17,21,30,35,38,49,52,56,59,74,87,100,104,107,119,131,135,141,145,211,215,219,222,228,232,234,240,244,246,252,256,259,265,268,290],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-hash-passwords-securely",[18,19,20],"p",{},"The difference between \"encrypted\" and actually secure",[22,23,24,27],"tldr",{},[18,25,26],{},"TL;DR",[18,28,29],{},"Use bcrypt with a cost factor of 10-12, or Argon2id if you need maximum security. Never use MD5, SHA1, or SHA256 for passwords. Hash on registration, compare on login. Never store plaintext passwords.",[31,32,34],"h2",{"id":33},"why-regular-hashing-isnt-enough","Why Regular Hashing Isn't Enough",[18,36,37],{},"Never Use These for Passwords",[39,40,45],"pre",{"className":41,"code":43,"language":44},[42],"language-text","// INSECURE: These can be cracked in seconds\nconst crypto = require('crypto');\n\n// MD5 - completely broken\ncrypto.createHash('md5').update(password).digest('hex');\n\n// SHA1 - also broken\ncrypto.createHash('sha1').update(password).digest('hex');\n\n// SHA256 - too fast, easily brute-forced\ncrypto.createHash('sha256').update(password).digest('hex');\n","text",[46,47,43],"code",{"__ignoreMap":48},"",[18,50,51],{},"These algorithms are fast by design. An attacker can try billions of passwords per second.",[31,53,55],{"id":54},"option-1-bcrypt-recommended","Option 1: bcrypt (Recommended)",[18,57,58],{},"bcrypt is the most widely used password hashing algorithm. It's deliberately slow and includes a salt automatically.",[60,61,63,68],"step",{"number":62},"1",[64,65,67],"h3",{"id":66},"install-bcrypt","Install bcrypt",[39,69,72],{"className":70,"code":71,"language":44},[42],"npm install bcrypt\nnpm install @types/bcrypt --save-dev  # TypeScript\n",[46,73,71],{"__ignoreMap":48},[60,75,77,81],{"number":76},"2",[64,78,80],{"id":79},"hash-password-on-registration","Hash password on registration",[39,82,85],{"className":83,"code":84,"language":44},[42],"import bcrypt from 'bcrypt';\n\nasync function registerUser(email: string, password: string) {\n  // Cost factor of 10-12 is recommended\n  // Higher = slower = more secure, but uses more CPU\n  const saltRounds = 10;\n\n  const hashedPassword = await bcrypt.hash(password, saltRounds);\n\n  // Store hashedPassword in database (NOT the original password)\n  await db.user.create({\n    data: {\n      email,\n      password: hashedPassword, // e.g., \"$2b$10$N9qo8...\"\n    }\n  });\n}\n",[46,86,84],{"__ignoreMap":48},[60,88,90,94],{"number":89},"3",[64,91,93],{"id":92},"verify-password-on-login","Verify password on login",[39,95,98],{"className":96,"code":97,"language":44},[42],"async function loginUser(email: string, password: string) {\n  const user = await db.user.findUnique({ where: { email } });\n\n  if (!user) {\n    throw new Error('Invalid credentials');\n  }\n\n  // Compare submitted password with stored hash\n  const isValid = await bcrypt.compare(password, user.password);\n\n  if (!isValid) {\n    throw new Error('Invalid credentials');\n  }\n\n  // Password is correct, create session\n  return createSession(user);\n}\n",[46,99,97],{"__ignoreMap":48},[31,101,103],{"id":102},"option-2-argon2-maximum-security","Option 2: Argon2 (Maximum Security)",[18,105,106],{},"Argon2id is the winner of the Password Hashing Competition and offers the best security. Use it for high-security applications.",[60,108,109,113],{"number":62},[64,110,112],{"id":111},"install-argon2","Install argon2",[39,114,117],{"className":115,"code":116,"language":44},[42],"npm install argon2\n",[46,118,116],{"__ignoreMap":48},[60,120,121,125],{"number":76},[64,122,124],{"id":123},"hash-and-verify","Hash and verify",[39,126,129],{"className":127,"code":128,"language":44},[42],"import argon2 from 'argon2';\n\n// Hash password\nasync function hashPassword(password: string) {\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\n// Verify password\nasync function verifyPassword(hash: string, password: string) {\n  return argon2.verify(hash, password);\n}\n\n// Usage\nconst hash = await hashPassword('userPassword123');\nconst isValid = await verifyPassword(hash, 'userPassword123');\n",[46,130,128],{"__ignoreMap":48},[31,132,134],{"id":133},"complete-example-nextjs-api-route","Complete Example: Next.js API Route",[39,136,139],{"className":137,"code":138,"language":44},[42],"// app/api/auth/register/route.ts\nimport bcrypt from 'bcrypt';\nimport { prisma } from '@/lib/prisma';\n\nexport async function POST(request: Request) {\n  const { email, password } = await request.json();\n\n  // Validate input\n  if (!email || !password) {\n    return Response.json(\n      { error: 'Email and password required' },\n      { status: 400 }\n    );\n  }\n\n  if (password.length \u003C 8) {\n    return Response.json(\n      { error: 'Password must be at least 8 characters' },\n      { status: 400 }\n    );\n  }\n\n  // Check if user exists\n  const existingUser = await prisma.user.findUnique({ where: { email } });\n  if (existingUser) {\n    return Response.json(\n      { error: 'Email already registered' },\n      { status: 409 }\n    );\n  }\n\n  // Hash password\n  const hashedPassword = await bcrypt.hash(password, 10);\n\n  // Create user\n  const user = await prisma.user.create({\n    data: {\n      email,\n      password: hashedPassword,\n    },\n    select: {\n      id: true,\n      email: true,\n      // Never return the password hash\n    },\n  });\n\n  return Response.json(user, { status: 201 });\n}\n",[46,140,138],{"__ignoreMap":48},[31,142,144],{"id":143},"bcrypt-vs-argon2-which-to-choose","bcrypt vs Argon2: Which to Choose?",[146,147,148,164],"table",{},[149,150,151],"thead",{},[152,153,154,158,161],"tr",{},[155,156,157],"th",{},"Factor",[155,159,160],{},"bcrypt",[155,162,163],{},"Argon2id",[165,166,167,179,190,200],"tbody",{},[152,168,169,173,176],{},[170,171,172],"td",{},"Security",[170,174,175],{},"Excellent",[170,177,178],{},"Best available",[152,180,181,184,187],{},[170,182,183],{},"Ecosystem",[170,185,186],{},"Widely supported",[170,188,189],{},"Growing support",[152,191,192,195,198],{},[170,193,194],{},"Installation",[170,196,197],{},"Native bindings",[170,199,197],{},[152,201,202,205,208],{},[170,203,204],{},"Recommendation",[170,206,207],{},"Most apps",[170,209,210],{},"High-security apps",[31,212,214],{"id":213},"common-mistakes","Common Mistakes",[64,216,218],{"id":217},"hashing-on-the-client-side","Hashing on the Client Side",[18,220,221],{},"Don't Do This",[39,223,226],{"className":224,"code":225,"language":44},[42],"// WRONG: Client-side hashing\nconst hash = await bcrypt.hash(password, 10);\nawait fetch('/api/register', { body: JSON.stringify({ hash }) });\n\n// The hash becomes the password! Attacker can use it directly.\n",[46,227,225],{"__ignoreMap":48},[64,229,231],{"id":230},"using-a-fixed-salt","Using a Fixed Salt",[18,233,221],{},[39,235,238],{"className":236,"code":237,"language":44},[42],"// WRONG: Same salt for all passwords\nconst salt = 'mysecuresalt123';\nconst hash = crypto.createHash('sha256').update(salt + password).digest('hex');\n\n// bcrypt generates a unique salt automatically - use it!\n",[46,239,237],{"__ignoreMap":48},[64,241,243],{"id":242},"comparing-hashes-directly","Comparing Hashes Directly",[18,245,221],{},[39,247,250],{"className":248,"code":249,"language":44},[42],"// WRONG: Direct comparison\nif (storedHash === bcrypt.hashSync(password, 10)) {\n  // This will never match! Each hash is unique.\n}\n\n// RIGHT: Use the compare function\nif (await bcrypt.compare(password, storedHash)) {\n  // This works correctly\n}\n",[46,251,249],{"__ignoreMap":48},[31,253,255],{"id":254},"migrating-from-insecure-hashing","Migrating from Insecure Hashing",[18,257,258],{},"If you have existing MD5/SHA hashes, migrate gradually:",[39,260,263],{"className":261,"code":262,"language":44},[42],"async function loginWithMigration(email: string, password: string) {\n  const user = await db.user.findUnique({ where: { email } });\n\n  if (user.password.startsWith('$2b$')) {\n    // Already bcrypt\n    return bcrypt.compare(password, user.password);\n  }\n\n  // Legacy MD5 hash\n  const md5Hash = crypto.createHash('md5').update(password).digest('hex');\n  if (md5Hash === user.password) {\n    // Upgrade to bcrypt\n    const newHash = await bcrypt.hash(password, 10);\n    await db.user.update({\n      where: { id: user.id },\n      data: { password: newHash },\n    });\n    return true;\n  }\n\n  return false;\n}\n",[46,264,262],{"__ignoreMap":48},[18,266,267],{},"Password Security Checklist",[269,270,271,275,278,281,284,287],"ul",{},[272,273,274],"li",{},"Use bcrypt (10+ rounds) or Argon2id",[272,276,277],{},"Hash on the server, never the client",[272,279,280],{},"Use the library's compare function",[272,282,283],{},"Never log or expose password hashes",[272,285,286],{},"Require minimum 8 character passwords",[272,288,289],{},"Consider checking against breached password lists",[291,292,293,299,304],"related-articles",{},[294,295],"related-card",{"description":296,"href":297,"title":298},"Step-by-step guide to secure session management. Create, store, validate, and expire sessions properly to protect user a","/blog/how-to/session-management","How to Implement Secure Session Management",[294,300],{"description":301,"href":302,"title":303},"Step-by-step guide to configuring CORS in Next.js, Express, and serverless functions. Avoid security mistakes and fix co","/blog/how-to/setup-cors-properly","How to Set Up CORS Properly",[294,305],{"description":306,"href":307,"title":308},"Step-by-step guide to setting up Row Level Security in Supabase. Enable RLS, write policies, test access, and avoid comm","/blog/how-to/setup-supabase-rls","How to Set Up Supabase Row Level Security (RLS)",{"title":48,"searchDepth":310,"depth":310,"links":311},2,[312,313,319,323,324,325,330],{"id":33,"depth":310,"text":34},{"id":54,"depth":310,"text":55,"children":314},[315,317,318],{"id":66,"depth":316,"text":67},3,{"id":79,"depth":316,"text":80},{"id":92,"depth":316,"text":93},{"id":102,"depth":310,"text":103,"children":320},[321,322],{"id":111,"depth":316,"text":112},{"id":123,"depth":316,"text":124},{"id":133,"depth":310,"text":134},{"id":143,"depth":310,"text":144},{"id":213,"depth":310,"text":214,"children":326},[327,328,329],{"id":217,"depth":316,"text":218},{"id":230,"depth":316,"text":231},{"id":242,"depth":316,"text":243},{"id":254,"depth":310,"text":255},"how-to","2026-01-14","Step-by-step guide to password hashing with bcrypt and Argon2. Why you should never use MD5 or SHA, and how to implement secure password storage in Node.js.",false,"md",null,"yellow",{},true,"Password hashing with bcrypt and Argon2. Why MD5 and SHA are not enough.","/blog/how-to/hash-passwords-securely","[object Object]","HowTo",{"title":5,"description":333},{"loc":341},"blog/how-to/hash-passwords-securely",[],"summary_large_image","u3L-ZmbsHZsT-zRtMpbX7o71DWLQBLv2z60CgDfDhas",1775843928368]