secure-auth

安装量: 49
排名: #15280

安装

npx skills add https://github.com/jamditis/claude-skills-journalism --skill secure-auth

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

返回排行榜