Authentication is one of the most critical parts of any web application. In 2025, the landscape has evolved significantly beyond simple username/password + sessions. This article explores production-ready, secure, and scalable authentication patterns in Node.js and Express.js and the best practices used by companies like Vercel, Supabase, Clerk (internally), and many startups building secure authentication systems.

authentication patterns

The Death of Traditional Sessions (And Why That’s Good)

Traditional cookie-based sessions with express-session + Redis/MemoryStore are still common, but they’re increasingly being replaced by stateless JWT + HttpOnly cookies or token-based patterns.

Problems with Traditional Sessions

  • Server-side state (hard to scale horizontally)
  • Requires sticky sessions in load balancers
  • Vulnerable to CSRF if not carefully mitigated
  • Poor mobile/API experience

This is the de facto standard in 2025 for web applications needing both security and good UX.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// middleware/auth.js
import jwt from 'jsonwebtoken';
import { promisify } from 'util';

const verifyJwt = promisify(jwt.verify);

export const authenticateToken = async (req, res, next) => {
  const token = req.cookies.access_token;

  if (!token) {
    return res.status(401).json({ message: 'Access token required' });
  }

  try {
    const payload = await verifyJwt(token, process.env.JWT_SECRET);
    req.user = payload;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ message: 'Token expired' });
    }
    return res.status(403).json({ message: 'Invalid token' });
  }
};
1
2
3
4
5
6
7
// Set access token (short-lived: 15 minutes)
res.cookie('access_token', accessToken, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict', // or 'lax' if you need third-party login
  maxAge: 15 * 60 * 1000 // 15 minutes
});

Refresh Token Rotation + Reuse Detection (The Gold Standard)

Never use long-lived JWTs. Use short-lived access tokens + rotating refresh tokens.

1
2
3
4
5
6
7
8
// Generate rotating refresh token
function generateRefreshToken(userId, refreshTokenId) {
  return jwt.sign(
    { sub: userId, jti: refreshTokenId, type: 'refresh' },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );
}

Database Schema (Prisma example)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
model User {
  id            String    @id @default(cuid())
  email         String    @unique
  passwordHash  String
  refreshTokens RefreshToken[]
}

model RefreshToken {
  id          String   @id @default(cuid())
  userId      String
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  tokenHash   String   @unique // hashed version stored
  expiresAt   DateTime
  createdAt   DateTime @default(now())
  revokedAt   DateTime?
  
  @@index([userId])
}

Refresh Token Endpoint with Rotation & Reuse Detection

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  if (!refreshToken) return res.sendStatus(401);

  let payload;
  try {
    payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
  } catch (err) {
    return res.sendStatus(403);
  }

  // Check if token was revoked or reused
  const storedToken = await prisma.refreshToken.findUnique({
    {
    where: { id: payload.jti }
  });

  if (!storedToken || storedToken.revokedAt) {
    // Possible token theft!
    await prisma.refreshToken.updateMany({
      where: { userId: payload.sub },
      data: { revokedAt: new Date() }
    });
    res.clearCookie('refresh_token');
    return res.status(403).json({ message: 'Token compromised' });
  }

  if (storedToken.expiresAt < new Date()) {
    return res.sendStatus(403);
  }

  // Rotate refresh token
  const newRefreshTokenId = crypto.randomUUID();
  const newRefreshToken = generateRefreshToken(payload.sub, newRefreshTokenId);

  // Hash and store new token
  const hashedToken = await hashToken(newRefreshToken);

  await prisma.refreshToken.create({
    data: {
      id: newRefreshTokenId,
      userId: payload.sub,
      tokenHash: hashedToken,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    }
  });

  // Revoke old token
  await prisma.refreshToken.update({
    where: { id: payload.jti },
    data: { revokedAt: new Date() }
  });

  // Issue new access token
  const accessToken = generateAccessToken(payload.sub);

  res.cookie('access_token', accessToken, accessCookieOptions);
  res.cookie('refresh_token', newRefreshToken, refreshCookieOptions);

  res.json({ message: 'Tokens renewed' });
});

Silent Authentication & Token Renewal (For SPA UX)

Keep users logged in without page refreshes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Frontend: Silent refresh before access token expires (e.g., at 14 minutes)
setInterval(async () => {
  try {
    const res = await fetch('/api/auth/refresh', { credentials: 'include' });
    if (!res.ok) throw new Error('Refresh failed');
  } catch (err) {
    // Redirect to login
    window.location.href = '/login';
  }
}, 14 * 60 * 1000); // Every 14 minutes

Extremely popular for developer tools and low-friction apps.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
app.post('/api/auth/magic-link', async (req, res) => {
  const { email } = req.body;
  
  const user = await prisma.user.findUnique({ where: { email } });
  if (!user) return res.status(200).json({ message: 'If email exists, link sent' });

  const token = crypto.randomBytes(32).toString('hex');
  const hashedToken = await bcrypt.hash(token, 12);

  await prisma.magicToken.create({
    data: {
      userId: user.id,
      tokenHash: hashedToken,
      expiresAt: new Date(Date.now() + 15 * 60 * 1000) // 15 min
    }
  });

  const link = `${process.env.APP_URL}/auth/magic?token=${token}&email=${encodeURIComponent(email)}`;
  
  await sendEmail(email, 'Your Magic Login Link', `Click here: ${link}`);

  res.json({ message: 'Magic link sent' });
});

Passkeys / WebAuthn (The Future (Already Here)

Hardware-backed, phishing-resistant authentication.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Using simplewebauthn library (recommended)
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';

app.get('/api/auth/passkey/register/options', async (req, res) => {
  const user = req.user; // from session or magic link

  const options = generateRegistrationOptions({
    rpName: 'My Awesome App',
    rpID: 'localhost',
    userID: user.id,
    userName: user.email,
    attestationType: 'none',
    excludeCredentials: [], // existing credentials
  });

  // Store challenge in DB or session
  await prisma.challenge.create({
    data: {
      userId: user.id,
      challenge: options.challenge,
      type: 'registration'
    }
  });

  res.json(options);
});

Social OAuth2 with Proper Implementation

Never trust the provider’s ID token alone. Always verify with their API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Example: Google OAuth2
app.get('/auth/google/callback', async (req, res) => {
  const { code } = req.query;
  
  const { tokens } = await oauth2Client.getToken(code);
  
  // Verify ID token properly
  const ticket = await client.verifyIdToken({
    idToken: tokens.id_token,
    audience: process.env.GOOGLE_CLIENT_ID
  });
  
  const payload = ticket.getPayload();
  
  let user = await prisma.user.findUnique({ where: { email: payload.email } });
  
  if (!user) {
    user = await prisma.user.create({
      data: {
        email: payload.email,
        name: payload.name,
        avatar: payload.picture,
        emailVerified: payload.email_verified
      }
    });
  }

  // Issue your own tokens
  const accessToken = generateAccessToken(user.id);
  // ... set cookies
});

Rate Limiting & Brute Force Protection

Essential for any auth endpoint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import rateLimit from 'express-rate-limit';

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window per IP
  message: 'Too many login attempts',
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => {
    return req.ip || req.headers['x-forwarded-for'];
  }
});

app.post('/api/auth/login', loginLimiter, loginHandler);

Multi-Factor Authentication (TOTP + Backup Codes)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Enable TOTP
app.post('/api/auth/mfa/totp/enable', authenticateToken, async (req, res) => {
  const secret = speakeasy.generateSecret({
    name: `MyApp (${req.user.email})`
  });

  await prisma.user.update({
    where: { id: req.user.id },
    data: {
      totpSecret: secret.base32,
      backupCodes: generateBackupCodes() // 10 one-time codes
    }
  });

  res.json({
    secret: secret.base32,
    otpauth_url: secret.otpauth_url
  });
});

Best Practices Summary (2025)

Pattern Best For Security Level Phishing Resistant Passwordless
JWT + HttpOnly Cookies + Refresh Rotation Most production web apps Very High Yes Yes
Magic Links SaaS, dev tools, internal apps High Yes Yes
Passkeys (WebAuthn / FIDO2) Banking, crypto, enterprise Highest Yes Yes
Traditional Sessions + Redis Legacy apps, simple projects Medium No No
Social OAuth2 (Google, GitHub, etc.) Consumer apps, quick signup High (if implemented correctly) No Yes

Conclusion: Final Recommendations

  • Never store refresh tokens in plain text → always hash them
  • Use HttpOnly, Secure, SameSite cookies → no localStorage JWTs
  • Implement refresh token rotation + reuse detection
  • Rate limit all auth endpoints
  • Use short-lived access tokens (15-60 min)
  • Consider passkeys for new applications

Authentication security is not about choosing one perfect method; it’s about defense in depth, proper implementation, and staying updated with evolving threats. Stay Adaptive, Stay secure! 🔐