
secure-auth
Secure authentication implementation patterns. Use when implementing user login, registration, password reset, session management, JWT authentication, or OAuth integration. Provides production-ready patterns that avoid common tutorial pitfalls like insecure token storage, weak password hashing, and session fixation.
Secure authentication implementation patterns. Use when implementing user login, registration, password reset, session management, JWT authentication, or OAuth integration. Provides production-ready patterns that avoid common tutorial pitfalls like insecure token storage, weak password hashing, and session fixation.
Secure authentication
Production-ready authentication patterns. These aren't the simplest implementations—they're the ones that won't get you sued.
Authentication architecture decision
Sessions vs JWTs
Use sessions when:
- Server-rendered application
- Need immediate logout/revocation
- Single domain
- Simpler to implement correctly
Use JWTs when:
- Multiple services need to verify auth
- Stateless architecture required
- Mobile app + API
- Third-party integrations
Common mistake: Using JWTs because a tutorial did, then storing them in localStorage (XSS vulnerable) and having no revocation strategy.
Session-based authentication
Complete Express.js implementation
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const app = express();
// Redis client for session storage
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect();
// Session configuration
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET, // At least 32 random bytes
name: 'sessionId', // Don't use default 'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
httpOnly: true, // Not accessible via JavaScript
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Rate limiting for auth endpoints
const loginAttempts = new Map();
function checkRateLimit(ip) {
const attempts = loginAttempts.get(ip) || { count: 0, resetAt: Date.now() + 900000 };
if (Date.now() > attempts.resetAt) {
attempts.count = 0;
attempts.resetAt = Date.now() + 900000; // 15 minute window
}
if (attempts.count >= 5) {
return false;
}
attempts.count++;
loginAttempts.set(ip, attempts);
return true;
}
// Registration
app.post('/auth/register', async (req, res) => {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
if (password.length < 12) {
return res.status(400).json({ error: 'Password must be at least 12 characters' });
}
// Check if user exists
const existingUser = await db.query(
'SELECT id FROM users WHERE email = $1',
[email.toLowerCase()]
);
if (existingUser.rows.length > 0) {
// Don't reveal if email exists - use same message/timing
return res.status(400).json({ error: 'Registration failed' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create user
const result = await db.query(
'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id',
[email.toLowerCase(), hashedPassword]
);
// Create session
req.session.userId = result.rows[0].id;
req.session.createdAt = Date.now();
res.json({ success: true });
});
// Login
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const clientIp = req.ip;
// Rate limiting
if (!checkRateLimit(clientIp)) {
return res.status(429).json({ error: 'Too many attempts. Try again later.' });
}
// Validate input
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
// Find user
const result = await db.query(
'SELECT id, password_hash FROM users WHERE email = $1',
[email.toLowerCase()]
);
if (result.rows.length === 0) {
// Timing attack prevention: still do bcrypt compare
await bcrypt.compare(password, '$2b$12$invalidhashtopreventtiming');
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
// Verify password
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Regenerate session to prevent fixation
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ error: 'Session error' });
}
req.session.userId = user.id;
req.session.createdAt = Date.now();
// Clear rate limit on successful login
loginAttempts.delete(clientIp);
res.json({ success: true });
});
});
// Logout
app.post('/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('sessionId');
res.json({ success: true });
});
});
// Auth middleware
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Authentication required' });
}
// Optional: Check session age
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
if (Date.now() - req.session.createdAt > maxAge) {
req.session.destroy();
return res.status(401).json({ error: 'Session expired' });
}
next();
}
// Protected route
app.get('/api/profile', requireAuth, async (req, res) => {
const user = await db.query(
'SELECT id, email, created_at FROM users WHERE id = $1',
[req.session.userId]
);
res.json(user.rows[0]);
});
JWT authentication
Complete implementation with refresh tokens
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Token configuration
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
// Store refresh tokens (use Redis in production)
const refreshTokens = new Map();
function generateAccessToken(userId) {
return jwt.sign(
{ userId, type: 'access' },
ACCESS_TOKEN_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRY }
);
}
function generateRefreshToken(userId) {
const tokenId = crypto.randomBytes(32).toString('hex');
const token = jwt.sign(
{ userId, tokenId, type: 'refresh' },
REFRESH_TOKEN_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);
// Store token ID for revocation
refreshTokens.set(tokenId, {
userId,
createdAt: Date.now(),
revoked: false
});
return token;
}
// Login - returns both tokens
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
// ... validation and password check ...
const accessToken = generateAccessToken(user.id);
const refreshToken = generateRefreshToken(user.id);
// Set refresh token as httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
// Return access token in response body
res.json({ accessToken });
});
// Refresh endpoint
app.post('/auth/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
const decoded = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
// Check if token was revoked
const storedToken = refreshTokens.get(decoded.tokenId);
if (!storedToken || storedToken.revoked) {
return res.status(401).json({ error: 'Token revoked' });
}
// Generate new access token
const accessToken = generateAccessToken(decoded.userId);
res.json({ accessToken });
} catch (err) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
});
// Logout - revoke refresh token
app.post('/auth/logout', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (refreshToken) {
try {
const decoded = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
const storedToken = refreshTokens.get(decoded.tokenId);
if (storedToken) {
storedToken.revoked = true;
}
} catch (err) {
// Token invalid, no action needed
}
}
res.clearCookie('refreshToken');
res.json({ success: true });
});
// Auth middleware for protected routes
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required' });
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET);
if (decoded.type !== 'access') {
return res.status(401).json({ error: 'Invalid token type' });
}
req.userId = decoded.userId;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
Frontend token handling
// auth.js - Frontend token management
class AuthManager {
constructor() {
this.accessToken = null;
}
async login(email, password) {
const response = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Important for cookies
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
const { accessToken } = await response.json();
this.accessToken = accessToken;
return true;
}
async refreshToken() {
const response = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
this.accessToken = null;
throw new Error('Session expired');
}
const { accessToken } = await response.json();
this.accessToken = accessToken;
return accessToken;
}
async fetchWithAuth(url, options = {}) {
if (!this.accessToken) {
throw new Error('Not authenticated');
}
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
}
});
// If token expired, try to refresh and retry
if (response.status === 401) {
const body = await response.json();
if (body.code === 'TOKEN_EXPIRED') {
await this.refreshToken();
// Retry original request
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
}
});
}
}
return response;
}
async logout() {
await fetch('/auth/logout', {
method: 'POST',
credentials: 'include'
});
this.accessToken = null;
}
}
export const auth = new AuthManager();
Password reset flow
Secure implementation
const crypto = require('crypto');
// Request password reset
app.post('/auth/forgot-password', async (req, res) => {
const { email } = req.body;
// Always return success to prevent email enumeration
res.json({ message: 'If an account exists, a reset link has been sent.' });
// Find user (async, after response)
const result = await db.query(
'SELECT id FROM users WHERE email = $1',
[email.toLowerCase()]
);
if (result.rows.length === 0) {
return; // User doesn't exist, but don't reveal that
}
const user = result.rows[0];
// Generate secure token
const token = crypto.randomBytes(32).toString('hex');
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const expiresAt = new Date(Date.now() + 3600000); // 1 hour
// Store hashed token (not plain token)
await db.query(
'INSERT INTO password_resets (user_id, token_hash, expires_at) VALUES ($1, $2, $3)',
[user.id, tokenHash, expiresAt]
);
// Send email with plain token
const resetUrl = `https://yourapp.com/reset-password?token=${token}`;
await sendEmail(email, 'Password Reset', `Reset your password: ${resetUrl}`);
});
// Reset password
app.post('/auth/reset-password', async (req, res) => {
const { token, newPassword } = req.body;
if (!token || !newPassword) {
return res.status(400).json({ error: 'Token and new password required' });
}
if (newPassword.length < 12) {
return res.status(400).json({ error: 'Password must be at least 12 characters' });
}
// Hash the provided token to compare with stored hash
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
// Find valid reset token
const result = await db.query(
`SELECT user_id FROM password_resets
WHERE token_hash = $1 AND expires_at > NOW() AND used = false`,
[tokenHash]
);
if (result.rows.length === 0) {
return res.status(400).json({ error: 'Invalid or expired token' });
}
const userId = result.rows[0].user_id;
// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 12);
// Update password and invalidate token
await db.query('UPDATE users SET password_hash = $1 WHERE id = $2', [hashedPassword, userId]);
await db.query('UPDATE password_resets SET used = true WHERE token_hash = $1', [tokenHash]);
// Invalidate all existing sessions for this user
await db.query('DELETE FROM sessions WHERE user_id = $1', [userId]);
res.json({ success: true });
});
OAuth integration (Google example)
Server-side flow (recommended)
const { OAuth2Client } = require('google-auth-library');
const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI
);
// Step 1: Redirect to Google
app.get('/auth/google', (req, res) => {
// Generate state for CSRF protection
const state = crypto.randomBytes(32).toString('hex');
req.session.oauthState = state;
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['email', 'profile'],
state: state,
prompt: 'consent'
});
res.redirect(authUrl);
});
// Step 2: Handle callback
app.get('/auth/google/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state to prevent CSRF
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state parameter');
}
delete req.session.oauthState;
try {
// Exchange code for tokens
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);
// Get user info
const ticket = await oauth2Client.verifyIdToken({
idToken: tokens.id_token,
audience: process.env.GOOGLE_CLIENT_ID
});
const payload = ticket.getPayload();
const { sub: googleId, email, name, picture } = payload;
// Find or create user
let user = await db.query(
'SELECT id FROM users WHERE google_id = $1',
[googleId]
);
if (user.rows.length === 0) {
// Create new user
user = await db.query(
`INSERT INTO users (google_id, email, name, avatar_url)
VALUES ($1, $2, $3, $4) RETURNING id`,
[googleId, email, name, picture]
);
}
// Create session
req.session.regenerate((err) => {
if (err) {
return res.status(500).send('Session error');
}
req.session.userId = user.rows[0].id;
res.redirect('/dashboard');
});
} catch (error) {
console.error('OAuth error:', error);
res.status(400).send('Authentication failed');
}
});
Multi-factor authentication (TOTP)
Server implementation
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Enable MFA for user
app.post('/auth/mfa/enable', requireAuth, async (req, res) => {
// Generate secret
const secret = speakeasy.generateSecret({
name: `YourApp:${req.user.email}`,
issuer: 'YourApp'
});
// Store secret (encrypted) temporarily until verified
await db.query(
'UPDATE users SET mfa_secret_temp = $1 WHERE id = $2',
[encrypt(secret.base32), req.userId]
);
// Generate QR code
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
res.json({
secret: secret.base32, // Show this as backup
qrCode: qrCode
});
});
// Verify and activate MFA
app.post('/auth/mfa/verify', requireAuth, async (req, res) => {
const { code } = req.body;
const result = await db.query(
'SELECT mfa_secret_temp FROM users WHERE id = $1',
[req.userId]
);
const secret = decrypt(result.rows[0].mfa_secret_temp);
const verified = speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: code,
window: 1 // Allow 1 step tolerance
});
if (!verified) {
return res.status(400).json({ error: 'Invalid code' });
}
// Move secret from temp to permanent
await db.query(
'UPDATE users SET mfa_secret = mfa_secret_temp, mfa_secret_temp = NULL, mfa_enabled = true WHERE id = $1',
[req.userId]
);
res.json({ success: true });
});
// Login with MFA
app.post('/auth/login', async (req, res) => {
const { email, password, mfaCode } = req.body;
// ... verify email/password first ...
if (user.mfa_enabled) {
if (!mfaCode) {
return res.status(401).json({
error: 'MFA code required',
requiresMfa: true
});
}
const verified = speakeasy.totp.verify({
secret: decrypt(user.mfa_secret),
encoding: 'base32',
token: mfaCode,
window: 1
});
if (!verified) {
return res.status(401).json({ error: 'Invalid MFA code' });
}
}
// ... create session/token ...
});
Security considerations checklist
Password storage
- [ ] Using bcrypt/scrypt/Argon2 with cost factor 12+
- [ ] Never storing plain text passwords
- [ ] Never logging passwords
Session management
- [ ] Sessions stored server-side (not just in cookies)
- [ ] Session IDs are cryptographically random
- [ ] Sessions regenerated on login (prevent fixation)
- [ ] Sessions invalidated on logout
- [ ] Sessions have maximum lifetime
JWT security
- [ ] Short access token lifetime (15 min or less)
- [ ] Refresh tokens stored as httpOnly cookies
- [ ] Refresh token rotation implemented
- [ ] Token revocation mechanism exists
- [ ] Secrets are at least 256 bits
Rate limiting
- [ ] Login attempts limited per IP
- [ ] Account lockout after N failures
- [ ] Password reset requests limited
- [ ] MFA verification attempts limited
CSRF protection
- [ ] SameSite cookie attribute set
- [ ] CSRF tokens for state-changing operations
- [ ] OAuth state parameter verified
Information disclosure
- [ ] Same error messages for valid/invalid users
- [ ] Timing attacks mitigated
- [ ] No user enumeration via registration/reset
You Might Also Like
Related Skills

create-pr
Creates GitHub pull requests with properly formatted titles that pass the check-pr-title CI validation. Use when creating PRs, submitting changes for review, or when the user says /pr or asks to create a pull request.
n8n-io
electron-chromium-upgrade
Guide for performing Chromium version upgrades in the Electron project. Use when working on the roller/chromium/main branch to fix patch conflicts during `e sync --3`. Covers the patch application workflow, conflict resolution, analyzing upstream Chromium changes, and proper commit formatting for patch fixes.
electron
pr-creator
Use this skill when asked to create a pull request (PR). It ensures all PRs follow the repository's established templates and standards.
google-gemini
clawdhub
Use the ClawdHub CLI to search, install, update, and publish agent skills from clawdhub.com. Use when you need to fetch new skills on the fly, sync installed skills to latest or a specific version, or publish new/updated skill folders with the npm-installed clawdhub CLI.
moltbot
tmux
Remote-control tmux sessions for interactive CLIs by sending keystrokes and scraping pane output.
moltbot
create-pull-request
Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
cline