We've built a solid authentication system, but is it secure enough for production? In this final part, we'll harden our security, add social login with OAuth, and prepare for production deployment.
By the end, you'll have a battle-tested authentication system ready for real users.
What We're Covering
- ✅ Common security vulnerabilities and how to prevent them
- ✅ Social login with OAuth (Google and GitHub)
- ✅ Rate limiting and brute force protection
- ✅ Security headers and HTTPS configuration
- ✅ Production deployment checklist
Time estimate: 60-90 minutes
Part 1: Common Security Vulnerabilities
Let's revisit the vulnerabilities from Part 1 and implement specific protections.
Vulnerability Matrix
| Attack | Risk Level | What It Does | How We Prevent It |
|---|---|---|---|
| XSS | 🔴 Critical | Injects malicious scripts | httpOnly cookies, CSP headers, input sanitization |
| CSRF | 🟡 High | Forces unwanted actions | SameSite cookies, CSRF tokens, Origin validation |
| Session Hijacking | 🔴 Critical | Steals user sessions | HTTPS only, secure cookies, IP binding |
| Brute Force | 🟡 High | Guesses passwords | Rate limiting, account lockout, strong passwords |
| SQL Injection | 🔴 Critical | Manipulates database queries | Parameterized queries, ORM, input validation |
| Token Leakage | 🟡 High | Exposes auth tokens | Never log tokens, short expiration, httpOnly |
| Man-in-the-Middle | 🔴 Critical | Intercepts traffic | HTTPS/TLS, HSTS headers, certificate pinning |
1. XSS Protection
Implementation:
// src/middleware/security.ts
import helmet from 'helmet';
import { Express } from 'express';
export function configureSecurityHeaders(app: Express) {
// Use Helmet.js for security headers
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Adjust for your needs
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: { policy: 'same-site' },
dnsPrefetchControl: true,
frameguard: { action: 'deny' },
hidePoweredBy: true,
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
ieNoOpen: true,
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
xssFilter: true,
})
);
}
// Sanitize user input
import validator from 'validator';
export function sanitizeInput(input: string): string {
return validator.escape(input);
}
export function validateEmail(email: string): boolean {
return validator.isEmail(email);
}
Install dependencies:
npm install helmet validator
npm install -D @types/validator
2. CSRF Protection
// src/middleware/csrf.ts
import csrf from 'csurf';
import cookieParser from 'cookie-parser';
// CSRF protection for state-changing operations
export const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
},
});
// Middleware to add CSRF token to responses
export function addCsrfToken(req: any, res: any, next: any) {
res.locals.csrfToken = req.csrfToken();
next();
}
// In routes that need CSRF protection:
// router.post('/api/sensitive', csrfProtection, handler);
Install:
npm install csurf
npm install -D @types/csurf
Frontend usage:
// Get CSRF token from meta tag or cookie
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
// Include in POST requests
fetch('/api/sensitive', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({ data: 'value' }),
});
3. Rate Limiting
Prevent brute force attacks with rate limiting:
// src/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';
// Create Redis client (for distributed rate limiting)
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
});
/**
* Strict rate limiter for auth endpoints
* 5 attempts per 15 minutes
*/
export const authRateLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:auth:',
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 requests per window
message: {
error: 'Too many login attempts',
message: 'Please try again after 15 minutes',
retryAfter: 900, // seconds
},
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
// Slow down instead of blocking (optional)
skipSuccessfulRequests: true, // Don't count successful logins
});
/**
* General API rate limiter
* 100 requests per 15 minutes
*/
export const apiRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: {
error: 'Too many requests',
message: 'Please slow down',
},
});
/**
* Aggressive rate limiter for sensitive operations
* 3 attempts per hour
*/
export const sensitiveRateLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:sensitive:',
}),
windowMs: 60 * 60 * 1000, // 1 hour
max: 3,
message: {
error: 'Too many attempts',
message: 'Account temporarily locked. Try again in 1 hour.',
},
});
Apply rate limiters:
// src/routes/auth.ts
import { authRateLimiter, sensitiveRateLimiter } from '../middleware/rateLimit';
// Login endpoint with rate limiting
router.post('/login', authRateLimiter, async (req, res) => {
// Login logic
});
// Password reset with aggressive rate limiting
router.post('/reset-password', sensitiveRateLimiter, async (req, res) => {
// Reset logic
});
Install:
npm install express-rate-limit rate-limit-redis ioredis
npm install -D @types/ioredis
4. SQL Injection Prevention
Always use parameterized queries:
// ❌ VULNERABLE - Never do this!
const query = `SELECT * FROM users WHERE username = '${username}'`;
await pool.query(query);
// ✅ SAFE - Use parameterized queries
const query = 'SELECT * FROM users WHERE username = $1';
await pool.query(query, [username]);
// ✅ EVEN BETTER - Use query builder or ORM
import { QueryBuilder } from 'knex';
const user = await db('users').where({ username }).first();
5. Secure Password Requirements
// src/utils/password.ts
import zxcvbn from 'zxcvbn';
export interface PasswordStrength {
score: number; // 0-4
feedback: string[];
isStrong: boolean;
}
/**
* Check password strength
* Returns score from 0 (weak) to 4 (strong)
*/
export function checkPasswordStrength(password: string): PasswordStrength {
const result = zxcvbn(password);
return {
score: result.score,
feedback: [
...result.feedback.suggestions,
...(result.feedback.warning ? [result.feedback.warning] : []),
],
isStrong: result.score >= 3, // Require score of 3 or 4
};
}
/**
* Validate password meets minimum requirements
*/
export function validatePassword(password: string): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain lowercase letters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain uppercase letters');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain numbers');
}
if (!/[^a-zA-Z0-9]/.test(password)) {
errors.push('Password must contain special characters');
}
const strength = checkPasswordStrength(password);
if (!strength.isStrong) {
errors.push('Password is too weak. ' + strength.feedback.join(' '));
}
return {
valid: errors.length === 0,
errors,
};
}
Install:
npm install zxcvbn
npm install -D @types/zxcvbn
Part 2: Social Login with OAuth
Let's implement "Sign in with Google" and "Sign in with GitHub".
OAuth Flow Overview
1. User clicks "Sign in with Google"
2. Redirect to Google's login page
3. User authenticates with Google
4. Google redirects back with authorization code
5. Exchange code for access token
6. Use token to get user info from Google
7. Create/update user in your database
8. Create your own session (JWT)
9. User is logged in!
Google OAuth Setup
1. Create OAuth credentials:
Visit Google Cloud Console:
- Create new project
- Enable Google+ API
- Create OAuth 2.0 Client ID
- Add authorized redirect URI:
http://localhost:3000/api/auth/google/callback
Add to .env:
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/google/callback
2. Install passport.js:
npm install passport passport-google-oauth20
npm install -D @types/passport @types/passport-google-oauth20
3. Configure Passport:
// src/config/passport.ts
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { findOrCreateUser } from '../models/User';
import { config } from './env';
passport.use(
new GoogleStrategy(
{
clientID: config.google.clientId,
clientSecret: config.google.clientSecret,
callbackURL: config.google.redirectUri,
},
async (accessToken, refreshToken, profile, done) => {
try {
// Extract user info from Google profile
const email = profile.emails?.[0]?.value;
const name = profile.displayName;
const googleId = profile.id;
if (!email) {
return done(new Error('No email from Google'), undefined);
}
// Find or create user in database
const user = await findOrCreateUser({
email,
username: email.split('@')[0],
name,
googleId,
provider: 'google',
});
return done(null, user);
} catch (error) {
return done(error as Error, undefined);
}
}
)
);
export default passport;
4. OAuth routes:
// src/routes/auth.ts
import passport from '../config/passport';
/**
* GET /api/auth/google
* Initiate Google OAuth flow
*/
router.get(
'/google',
passport.authenticate('google', {
scope: ['profile', 'email'],
session: false, // We're using JWT, not sessions
})
);
/**
* GET /api/auth/google/callback
* Google redirects here after authentication
*/
router.get(
'/google/callback',
passport.authenticate('google', {
session: false,
failureRedirect: '/login?error=google_auth_failed',
}),
async (req, res) => {
try {
const user = req.user as any;
// Generate our JWT tokens
const accessToken = generateAccessToken(
user.id.toString(),
user.username
);
const refreshToken = generateRefreshToken(
user.id.toString(),
user.username
);
// Store refresh token
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 30);
await storeRefreshToken(
user.id,
refreshToken,
expiresAt,
req.ip,
req.get('user-agent')
);
// Set cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000,
});
// Redirect to dashboard
res.redirect('/dashboard');
} catch (error) {
console.error('Google callback error:', error);
res.redirect('/login?error=callback_failed');
}
}
);
GitHub OAuth Setup
1. Create OAuth App:
Visit GitHub Settings:
- New OAuth App
- Callback URL:
http://localhost:3000/api/auth/github/callback
Add to .env:
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
GITHUB_REDIRECT_URI=http://localhost:3000/api/auth/github/callback
2. Configure Strategy:
npm install passport-github2
npm install -D @types/passport-github2
// src/config/passport.ts
import { Strategy as GitHubStrategy } from 'passport-github2';
passport.use(
new GitHubStrategy(
{
clientID: config.github.clientId,
clientSecret: config.github.clientSecret,
callbackURL: config.github.redirectUri,
},
async (accessToken: string, refreshToken: string, profile: any, done: any) => {
try {
const email = profile.emails?.[0]?.value;
const username = profile.username;
const name = profile.displayName;
const githubId = profile.id;
if (!email) {
return done(new Error('No email from GitHub'), undefined);
}
const user = await findOrCreateUser({
email,
username,
name,
githubId,
provider: 'github',
});
return done(null, user);
} catch (error) {
return done(error, undefined);
}
}
)
);
3. GitHub routes:
// src/routes/auth.ts
/**
* GET /api/auth/github
* Initiate GitHub OAuth flow
*/
router.get(
'/github',
passport.authenticate('github', {
scope: ['user:email'],
session: false,
})
);
/**
* GET /api/auth/github/callback
* GitHub redirects here after authentication
*/
router.get(
'/github/callback',
passport.authenticate('github', {
session: false,
failureRedirect: '/login?error=github_auth_failed',
}),
async (req, res) => {
// Same logic as Google callback
// ... (generate tokens, set cookies, redirect)
}
);
Frontend: Social Login Buttons
// LoginPage.tsx
export function LoginPage() {
return (
<div>
<h1>Login</h1>
{/* Traditional login */}
<form onSubmit={handleLogin}>
<input type="text" placeholder="Username" />
<input type="password" placeholder="Password" />
<button>Login</button>
</form>
{/* Social login */}
<div className="social-login">
<a
href="http://localhost:3000/api/auth/google"
className="btn btn-google"
>
<img src="/google-icon.svg" alt="" />
Continue with Google
</a>
<a
href="http://localhost:3000/api/auth/github"
className="btn btn-github"
>
<img src="/github-icon.svg" alt="" />
Continue with GitHub
</a>
</div>
</div>
);
}
Part 3: HTTPS Configuration
Never run authentication in production without HTTPS!
Development: Use mkcert
# Install mkcert
brew install mkcert # macOS
# or
choco install mkcert # Windows
# or
apt install mkcert # Linux
# Create local CA
mkcert -install
# Generate certificate
mkcert localhost 127.0.0.1 ::1
# Creates:
# - localhost+2.pem (certificate)
# - localhost+2-key.pem (private key)
Update server:
// src/server.ts
import https from 'https';
import fs from 'fs';
const httpsOptions = {
key: fs.readFileSync('./localhost+2-key.pem'),
cert: fs.readFileSync('./localhost+2.pem'),
};
https.createServer(httpsOptions, app).listen(3000, () => {
console.log('🔒 HTTPS server running on https://localhost:3000');
});
Production: Use Let's Encrypt
# Install certbot
sudo apt install certbot python3-certbot-nginx
# Get certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Auto-renewal (certbot sets this up automatically)
sudo certbot renew --dry-run
Nginx configuration:
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Strong SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS header
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Proxy to Node.js app
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
Part 4: Production Deployment Checklist
Environment Variables
# .env.production
NODE_ENV=production
# Database
DATABASE_URL=postgresql://user:pass@host:5432/dbname
DATABASE_SSL=true
# JWT
JWT_SECRET=<generate-64-char-random-string>
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=30d
# OAuth
GOOGLE_CLIENT_ID=production-client-id
GOOGLE_CLIENT_SECRET=production-secret
GOOGLE_REDIRECT_URI=https://yourdomain.com/api/auth/google/callback
GITHUB_CLIENT_ID=production-client-id
GITHUB_CLIENT_SECRET=production-secret
GITHUB_REDIRECT_URI=https://yourdomain.com/api/auth/github/callback
# Redis
REDIS_URL=redis://user:pass@host:6379
REDIS_TLS=true
# Monitoring
SENTRY_DSN=https://your-sentry-dsn
LOG_LEVEL=info
Security Checklist
- [ ] HTTPS enabled - All traffic encrypted
- [ ] HSTS header - Force HTTPS
- [ ] Secure cookies -
secure: trueflag set - [ ] httpOnly cookies - JavaScript can't access
- [ ] SameSite cookies - CSRF protection
- [ ] CSP headers - XSS protection
- [ ] Rate limiting - Brute force protection
- [ ] Input validation - All user input sanitized
- [ ] Parameterized queries - SQL injection protection
- [ ] Password strength - Minimum requirements enforced
- [ ] bcrypt hashing - Passwords properly hashed
- [ ] Token expiration - Short-lived access tokens
- [ ] Refresh token rotation - Tokens rotated on use
- [ ] Session management - Users can revoke sessions
- [ ] Error messages - Don't leak sensitive info
- [ ] Logging - Auth events logged (without sensitive data)
- [ ] Monitoring - Alerts for suspicious activity
Database Security
-- Create read-only user for application
CREATE ROLE app_user WITH LOGIN PASSWORD 'strong_password';
-- Grant minimal permissions
GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON refresh_tokens TO app_user;
GRANT SELECT ON content TO app_user;
-- Don't grant DELETE on users (handle via application logic)
-- Don't grant schema modification permissions
Docker Deployment
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- JWT_SECRET=${JWT_SECRET}
depends_on:
- db
- redis
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: auth_system
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:
redis_data:
Monitoring with Sentry
// src/server.ts
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
// Error handler
app.use(Sentry.Handlers.errorHandler());
Health Checks
// src/routes/health.ts
import express from 'express';
import { pool } from '../database/connection';
import { redis } from '../config/redis';
const router = express.Router();
router.get('/health', async (req, res) => {
const checks = {
server: 'ok',
database: 'checking',
redis: 'checking',
};
// Check database
try {
await pool.query('SELECT 1');
checks.database = 'ok';
} catch {
checks.database = 'error';
}
// Check Redis
try {
await redis.ping();
checks.redis = 'ok';
} catch {
checks.redis = 'error';
}
const allOk = Object.values(checks).every((status) => status === 'ok');
res.status(allOk ? 200 : 503).json({
status: allOk ? 'healthy' : 'unhealthy',
checks,
timestamp: new Date().toISOString(),
});
});
export default router;
Summary
You've learned:
- ✅ Security vulnerabilities - XSS, CSRF, SQL injection, and how to prevent them
- ✅ Rate limiting - Protect against brute force attacks
- ✅ Social login - OAuth with Google and GitHub
- ✅ HTTPS configuration - Development and production setup
- ✅ Production deployment - Complete checklist and Docker configuration
- ✅ Monitoring - Health checks and error tracking
Series Complete!
Congratulations! You've built a production-ready authentication system from scratch. You now understand:
- Authentication fundamentals - JWT vs sessions, security concepts
- Implementation - Login, logout, password hashing, tokens
- Route protection - Middleware, guards, frontend integration
- Advanced features - Remember Me, refresh tokens, protected content
- Security & production - Hardening, OAuth, deployment
What's Next?
Continue improving your authentication system:
- Add 2FA - Time-based one-time passwords (TOTP)
- Magic links - Passwordless email authentication
- Biometric auth - WebAuthn/FIDO2 integration
- Audit logging - Comprehensive security event tracking
- Geolocation - Block suspicious locations
- Device fingerprinting - Detect account takeovers
Resources
Series complete! ← Back to Part 1
Questions? Security is an ongoing process. Stay updated on new vulnerabilities and best practices. Never stop learning!