[{"data":1,"prerenderedAt":478},["ShallowReactive",2],{"blog-best-practices/authentication":3},{"id":4,"title":5,"body":6,"category":453,"date":454,"dateModified":454,"description":455,"draft":456,"extension":457,"faq":458,"featured":456,"headerVariant":463,"image":464,"keywords":464,"meta":465,"navigation":466,"ogDescription":467,"ogTitle":464,"path":468,"readTime":469,"schemaOrg":470,"schemaType":471,"seo":472,"sitemap":473,"stem":474,"tags":475,"twitterCard":476,"__hash__":477},"blog/blog/best-practices/authentication.md","Authentication Best Practices: Secure Login, Sessions, and Token Management",{"type":7,"value":8,"toc":439},"minimark",[9,20,29,34,37,89,98,102,105,120,124,127,136,141,160,164,167,176,180,183,192,196,199,208,217,221,224,233,237,240,249,253,325,353,381,385,388,408,427],[10,11,12],"tldr",{},[13,14,15,19],"p",{},[16,17,18],"strong",{},"The #1 authentication security best practice is using established auth libraries instead of building your own."," These 7 practices take about 60 minutes to implement and prevent 91% of authentication-related breaches. Focus on: hashing passwords with bcrypt or Argon2, storing sessions securely, implementing rate limiting on login, and adding MFA for sensitive accounts.",[21,22,23],"quotable-box",{},[24,25,26],"blockquote",{},[13,27,28],{},"\"Authentication is the front door to your application. A weak lock invites every attacker on the internet to try the handle.\"",[30,31,33],"h2",{"id":32},"rule-1-use-established-auth-libraries","Rule 1: Use Established Auth Libraries",[13,35,36],{},"Authentication is complex. Use battle-tested solutions:",[38,39,40,53],"table",{},[41,42,43],"thead",{},[44,45,46,50],"tr",{},[47,48,49],"th",{},"Framework",[47,51,52],{},"Recommended Library",[54,55,56,65,73,81],"tbody",{},[44,57,58,62],{},[59,60,61],"td",{},"Next.js",[59,63,64],{},"NextAuth.js (Auth.js)",[44,66,67,70],{},[59,68,69],{},"React SPA",[59,71,72],{},"Auth0, Clerk, Firebase Auth",[44,74,75,78],{},[59,76,77],{},"Node.js",[59,79,80],{},"Passport.js, express-session",[44,82,83,86],{},[59,84,85],{},"Full-stack",[59,87,88],{},"Supabase Auth, Firebase Auth",[90,91,92],"warning-box",{},[13,93,94,97],{},[16,95,96],{},"Do not build custom auth"," unless you have specific expertise. Custom auth implementations are the source of most authentication vulnerabilities.",[30,99,101],{"id":100},"best-practice-1-hash-passwords-correctly-5-min","Best Practice 1: Hash Passwords Correctly 5 min",[13,103,104],{},"Never store plain text passwords. Use a proper hashing algorithm:",[106,107,109],"code-block",{"label":108},"Password hashing with bcrypt",[110,111,116],"pre",{"className":112,"code":114,"language":115},[113],"language-text","import bcrypt from 'bcrypt';\n\nconst SALT_ROUNDS = 12; // Higher = more secure but slower\n\n// Hash password before storing\nasync function hashPassword(password) {\n  return await bcrypt.hash(password, SALT_ROUNDS);\n}\n\n// Verify password during login\nasync function verifyPassword(password, hash) {\n  return await bcrypt.compare(password, hash);\n}\n\n// Registration\nasync function registerUser(email, password) {\n  const hashedPassword = await hashPassword(password);\n\n  await db.user.create({\n    data: {\n      email,\n      password: hashedPassword, // Store the hash, never the plain password\n    },\n  });\n}\n\n// Login\nasync function loginUser(email, password) {\n  const user = await db.user.findUnique({ where: { email } });\n\n  if (!user) {\n    // Use same error for missing user and wrong password\n    throw new Error('Invalid credentials');\n  }\n\n  const valid = await verifyPassword(password, user.password);\n\n  if (!valid) {\n    throw new Error('Invalid credentials');\n  }\n\n  return user;\n}\n","text",[117,118,114],"code",{"__ignoreMap":119},"",[30,121,123],{"id":122},"best-practice-2-secure-session-management-10-min","Best Practice 2: Secure Session Management 10 min",[13,125,126],{},"Sessions need proper configuration to be secure:",[106,128,130],{"label":129},"Express session configuration",[110,131,134],{"className":132,"code":133,"language":115},[113],"import session from 'express-session';\nimport RedisStore from 'connect-redis';\n\napp.use(session({\n  store: new RedisStore({ client: redisClient }),\n  secret: process.env.SESSION_SECRET, // Long random string\n  name: 'sessionId', // Custom name (not 'connect.sid')\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    httpOnly: true,       // Prevents JavaScript access\n    secure: true,         // HTTPS only\n    sameSite: 'lax',      // CSRF protection\n    maxAge: 24 * 60 * 60 * 1000, // 24 hours\n  },\n}));\n",[117,135,133],{"__ignoreMap":119},[137,138,140],"h4",{"id":139},"session-cookie-requirements","Session Cookie Requirements:",[142,143,144,148,151,154,157],"ul",{},[145,146,147],"li",{},"HttpOnly: true (prevents XSS from stealing cookies)",[145,149,150],{},"Secure: true (HTTPS only in production)",[145,152,153],{},"SameSite: 'lax' or 'strict' (CSRF protection)",[145,155,156],{},"Reasonable expiry (24 hours or less for sensitive apps)",[145,158,159],{},"Stored server-side (Redis, database) not just in cookie",[30,161,163],{"id":162},"best-practice-3-implement-rate-limiting-5-min","Best Practice 3: Implement Rate Limiting 5 min",[13,165,166],{},"Prevent brute force attacks on login:",[106,168,170],{"label":169},"Login rate limiting",[110,171,174],{"className":172,"code":173,"language":115},[113],"import rateLimit from 'express-rate-limit';\n\n// Strict rate limit for login\nconst loginLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, // 15 minutes\n  max: 5, // 5 attempts\n  message: { error: 'Too many login attempts. Try again later.' },\n  standardHeaders: true,\n  skipSuccessfulRequests: true, // Don't count successful logins\n});\n\napp.post('/api/auth/login', loginLimiter, async (req, res) => {\n  // Login logic...\n});\n\n// Even stricter for password reset\nconst resetLimiter = rateLimit({\n  windowMs: 60 * 60 * 1000, // 1 hour\n  max: 3,\n});\n\napp.post('/api/auth/forgot-password', resetLimiter, async (req, res) => {\n  // Password reset logic...\n});\n",[117,175,173],{"__ignoreMap":119},[30,177,179],{"id":178},"best-practice-4-secure-password-requirements-10-min","Best Practice 4: Secure Password Requirements 10 min",[13,181,182],{},"Enforce reasonable password policies:",[106,184,186],{"label":185},"Password validation",[110,187,190],{"className":188,"code":189,"language":115},[113],"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) => /[A-Z]/.test(password),\n    'Must contain at least one uppercase letter'\n  )\n  .refine(\n    (password) => /[a-z]/.test(password),\n    'Must contain at least one lowercase letter'\n  )\n  .refine(\n    (password) => /[0-9]/.test(password),\n    'Must contain at least one number'\n  );\n\n// Check against common passwords\nimport commonPasswords from './common-passwords.json';\n\nconst isCommonPassword = (password) => {\n  return commonPasswords.includes(password.toLowerCase());\n};\n\nfunction validatePassword(password) {\n  const result = passwordSchema.safeParse(password);\n\n  if (!result.success) {\n    return { valid: false, error: result.error.issues[0].message };\n  }\n\n  if (isCommonPassword(password)) {\n    return { valid: false, error: 'This password is too common' };\n  }\n\n  return { valid: true };\n}\n",[117,191,189],{"__ignoreMap":119},[30,193,195],{"id":194},"best-practice-5-secure-token-handling-jwt-10-min","Best Practice 5: Secure Token Handling (JWT) 10 min",[13,197,198],{},"If using JWTs, follow these guidelines:",[106,200,202],{"label":201},"Secure JWT implementation",[110,203,206],{"className":204,"code":205,"language":115},[113],"import jwt from 'jsonwebtoken';\n\nconst JWT_SECRET = process.env.JWT_SECRET; // Long random string\nconst ACCESS_TOKEN_EXPIRY = '15m'; // Short-lived\nconst REFRESH_TOKEN_EXPIRY = '7d'; // Longer-lived\n\n// Create tokens\nfunction generateTokens(userId) {\n  const accessToken = jwt.sign(\n    { userId, type: 'access' },\n    JWT_SECRET,\n    { expiresIn: ACCESS_TOKEN_EXPIRY }\n  );\n\n  const refreshToken = jwt.sign(\n    { userId, type: 'refresh' },\n    JWT_SECRET,\n    { expiresIn: REFRESH_TOKEN_EXPIRY }\n  );\n\n  return { accessToken, refreshToken };\n}\n\n// Verify token\nfunction verifyToken(token, expectedType) {\n  try {\n    const decoded = jwt.verify(token, JWT_SECRET);\n\n    if (decoded.type !== expectedType) {\n      throw new Error('Invalid token type');\n    }\n\n    return decoded;\n  } catch (error) {\n    throw new Error('Invalid token');\n  }\n}\n\n// Store refresh tokens in database for revocation\nasync function storeRefreshToken(userId, token) {\n  await db.refreshToken.create({\n    data: { userId, token, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) }\n  });\n}\n",[117,207,205],{"__ignoreMap":119},[209,210,211],"info-box",{},[13,212,213,216],{},[16,214,215],{},"JWT Best Practices:"," Use short expiry for access tokens (15 minutes), store refresh tokens server-side for revocation, use HttpOnly cookies instead of localStorage when possible.",[30,218,220],{"id":219},"best-practice-6-implement-logout-properly-5-min","Best Practice 6: Implement Logout Properly 5 min",[13,222,223],{},"Logout should invalidate the session completely:",[106,225,227],{"label":226},"Proper logout implementation",[110,228,231],{"className":229,"code":230,"language":115},[113],"app.post('/api/auth/logout', authenticate, async (req, res) => {\n  // For sessions: destroy the session\n  req.session.destroy((err) => {\n    if (err) {\n      console.error('Session destruction error:', err);\n    }\n  });\n\n  // For JWTs with refresh tokens: revoke the refresh token\n  await db.refreshToken.deleteMany({\n    where: { userId: req.user.id }\n  });\n\n  // Clear cookies\n  res.clearCookie('sessionId', {\n    httpOnly: true,\n    secure: true,\n    sameSite: 'lax',\n  });\n\n  res.json({ success: true });\n});\n",[117,232,230],{"__ignoreMap":119},[30,234,236],{"id":235},"best-practice-7-account-recovery-15-min","Best Practice 7: Account Recovery 15 min",[13,238,239],{},"Password reset flows need careful security:",[106,241,243],{"label":242},"Secure password reset flow",[110,244,247],{"className":245,"code":246,"language":115},[113],"import crypto from 'crypto';\n\n// Request password reset\napp.post('/api/auth/forgot-password', async (req, res) => {\n  const { email } = req.body;\n\n  // Always return same response (prevent user enumeration)\n  res.json({ message: 'If an account exists, a reset link was sent.' });\n\n  const user = await db.user.findUnique({ where: { email } });\n  if (!user) return; // Silent fail\n\n  // Generate secure token\n  const token = crypto.randomBytes(32).toString('hex');\n  const expiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour\n\n  // Hash token before storing (like a password)\n  const hashedToken = await bcrypt.hash(token, 10);\n\n  await db.passwordReset.create({\n    data: { userId: user.id, token: hashedToken, expiresAt: expiry }\n  });\n\n  // Send email with unhashed token\n  await sendEmail(email, `Reset link: .../reset?token=${token}`);\n});\n\n// Reset password\napp.post('/api/auth/reset-password', async (req, res) => {\n  const { token, newPassword } = req.body;\n\n  // Find all non-expired reset requests\n  const resetRequests = await db.passwordReset.findMany({\n    where: { expiresAt: { gt: new Date() } },\n    include: { user: true },\n  });\n\n  // Check token against each (timing-safe)\n  let validRequest = null;\n  for (const request of resetRequests) {\n    if (await bcrypt.compare(token, request.token)) {\n      validRequest = request;\n      break;\n    }\n  }\n\n  if (!validRequest) {\n    return res.status(400).json({ error: 'Invalid or expired reset link' });\n  }\n\n  // Update password\n  const hashedPassword = await bcrypt.hash(newPassword, 12);\n  await db.user.update({\n    where: { id: validRequest.userId },\n    data: { password: hashedPassword },\n  });\n\n  // Delete all reset tokens and sessions for this user\n  await db.passwordReset.deleteMany({ where: { userId: validRequest.userId } });\n  await db.session.deleteMany({ where: { userId: validRequest.userId } });\n\n  res.json({ success: true });\n});\n",[117,248,246],{"__ignoreMap":119},[30,250,252],{"id":251},"common-authentication-mistakes","Common Authentication Mistakes",[38,254,255,268],{},[41,256,257],{},[44,258,259,262,265],{},[47,260,261],{},"Mistake",[47,263,264],{},"Impact",[47,266,267],{},"Prevention",[54,269,270,281,292,303,314],{},[44,271,272,275,278],{},[59,273,274],{},"Plain text passwords",[59,276,277],{},"Mass credential theft",[59,279,280],{},"Always hash with bcrypt/Argon2",[44,282,283,286,289],{},[59,284,285],{},"User enumeration",[59,287,288],{},"Attackers learn valid emails",[59,290,291],{},"Same response for all cases",[44,293,294,297,300],{},[59,295,296],{},"No rate limiting",[59,298,299],{},"Brute force attacks",[59,301,302],{},"Limit login attempts",[44,304,305,308,311],{},[59,306,307],{},"Long JWT expiry",[59,309,310],{},"Token theft risk",[59,312,313],{},"Short-lived access tokens",[44,315,316,319,322],{},[59,317,318],{},"Incomplete logout",[59,320,321],{},"Session persistence",[59,323,324],{},"Clear all tokens/sessions",[209,326,327],{},[13,328,329,332,333,340,341,346,347,352],{},[16,330,331],{},"Official Resources:"," For comprehensive authentication guidance, see ",[334,335,339],"a",{"href":336,"rel":337},"https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html",[338],"nofollow","OWASP Authentication Cheat Sheet",", ",[334,342,345],{"href":343,"rel":344},"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html",[338],"OWASP Session Management Cheat Sheet",", and ",[334,348,351],{"href":349,"rel":350},"https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html",[338],"OWASP Password Storage Cheat Sheet",".",[354,355,356,363,369,375],"faq-section",{},[357,358,360],"faq-item",{"question":359},"Should I use JWT or sessions?",[13,361,362],{},"Sessions are simpler and more secure for most web apps. JWTs work better for APIs accessed by multiple clients. For SPAs, consider sessions stored in HttpOnly cookies.",[357,364,366],{"question":365},"How long should sessions last?",[13,367,368],{},"For general apps: 24 hours to 7 days with activity-based renewal. For sensitive apps (banking, medical): shorter sessions (1-4 hours) with re-authentication for critical actions.",[357,370,372],{"question":371},"Should I require MFA for all users?",[13,373,374],{},"MFA significantly improves security. Consider requiring it for admin accounts and sensitive operations. For general users, strongly encourage it but balance with usability.",[357,376,378],{"question":377},"Is OAuth safer than password auth?",[13,379,380],{},"OAuth delegates auth to providers like Google who have dedicated security teams. It reduces password-related risks but adds dependency on the provider. Consider offering both options.",[30,382,384],{"id":383},"further-reading","Further Reading",[13,386,387],{},"Put these practices into action with our step-by-step guides.",[142,389,390,396,402],{},[145,391,392],{},[334,393,395],{"href":394},"/blog/how-to/add-security-headers","Add security headers to your app",[145,397,398],{},[334,399,401],{"href":400},"/blog/checklists/pre-deployment-security-checklist","Pre-deployment security checklist",[145,403,404],{},[334,405,407],{"href":406},"/blog/getting-started/first-scan","Run your first security scan",[409,410,411,417,422],"related-articles",{},[412,413],"related-card",{"description":414,"href":415,"title":416},"Secure password handling","/blog/best-practices/password","Password Best Practices",[412,418],{"description":419,"href":420,"title":421},"Secure session handling","/blog/best-practices/session","Session Management",[412,423],{"description":424,"href":425,"title":426},"Token security patterns","/blog/best-practices/jwt","JWT Best Practices",[428,429,432,436],"cta-box",{"href":430,"label":431},"/","Start Free Scan",[30,433,435],{"id":434},"verify-your-authentication-security","Verify Your Authentication Security",[13,437,438],{},"Scan your application for authentication vulnerabilities.",{"title":119,"searchDepth":440,"depth":440,"links":441},2,[442,443,444,445,446,447,448,449,450,451,452],{"id":32,"depth":440,"text":33},{"id":100,"depth":440,"text":101},{"id":122,"depth":440,"text":123},{"id":162,"depth":440,"text":163},{"id":178,"depth":440,"text":179},{"id":194,"depth":440,"text":195},{"id":219,"depth":440,"text":220},{"id":235,"depth":440,"text":236},{"id":251,"depth":440,"text":252},{"id":383,"depth":440,"text":384},{"id":434,"depth":440,"text":435},"best-practices","2026-01-19","Authentication security best practices. Learn secure password handling, session management, JWT patterns, and OAuth implementation for web applications.",false,"md",[459,460,461,462],{"question":359,"answer":362},{"question":365,"answer":368},{"question":371,"answer":374},{"question":377,"answer":380},"vibe-green",null,{},true,"Build secure authentication with password hashing, session management, and token handling.","/blog/best-practices/authentication","16 min read","[object Object]","Article",{"title":5,"description":455},{"loc":468},"blog/best-practices/authentication",[],"summary_large_image","h0x4R6XUIQwbMoBjNzW7eo_fJLT4b7Opuwi1Jrilg1Y",1775843918547]