# Skill: Authentication & Security

## Identity
- **Skill ID**: `authentication-security`
- **Domain**: Authentication, Authorization, Security
- **Technologies**: JWT, bcrypt, SHA-256, API Keys, Rate Limiting
- **Source Agent**: `backend-app-expert.md`

## When to Load This Skill

Load this skill when working on:
- User registration and login endpoints
- API key generation and validation
- JWT token management (access/refresh)
- Password hashing and verification
- Rate limiting implementation
- Session management
- Security headers and middleware
- HMAC signature verification for webhooks

**File patterns:**
- `api/src/middleware/auth*.ts`
- `api/src/routes/auth*.ts`
- `api/src/utils/security*.ts`
- `api/src/middleware/rate-limit*.ts`

## Core Patterns

### 1. JWT Authentication (Admin Panel)

```typescript
import jwt from 'jsonwebtoken';
import { Response } from 'express';

// Token generation
interface TokenPayload {
  userId: string;
  email: string;
}

function generateTokens(payload: TokenPayload) {
  const accessToken = jwt.sign(
    payload,
    process.env.JWT_ACCESS_SECRET!,
    { expiresIn: '15m' } // Short-lived access token
  );

  const refreshToken = jwt.sign(
    payload,
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: '7d' } // Long-lived refresh token
  );

  return { accessToken, refreshToken };
}

// Set secure httpOnly cookies
function setAuthCookies(res: Response, accessToken: string, refreshToken: string) {
  res.cookie('access_token', accessToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 15 * 60 * 1000 // 15 minutes
  });

  res.cookie('refresh_token', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });
}

// Token verification middleware
import { Request, Response, NextFunction } from 'express';

interface AuthRequest extends Request {
  user?: TokenPayload;
}

async function requireAuth(req: AuthRequest, res: Response, next: NextFunction) {
  try {
    const token = req.cookies.access_token;
    
    if (!token) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET!) as TokenPayload;
    req.user = decoded;
    next();
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Refresh token endpoint
router.post('/auth/refresh', async (req, res) => {
  try {
    const refreshToken = req.cookies.refresh_token;
    
    if (!refreshToken) {
      return res.status(401).json({ error: 'Refresh token required' });
    }

    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as TokenPayload;
    
    // Generate new tokens
    const tokens = generateTokens({ userId: decoded.userId, email: decoded.email });
    setAuthCookies(res, tokens.accessToken, tokens.refreshToken);
    
    res.json({ success: true });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});
```

### 2. API Key Authentication (WordPress Plugin)

```typescript
import crypto from 'crypto';
import { prisma } from '../lib/prisma';

// Generate API key
async function generateApiKey(userId: string, name: string) {
  // Format: sk_live_{32 random chars}
  const randomBytes = crypto.randomBytes(24);
  const apiKey = `sk_live_${randomBytes.toString('hex')}`;
  
  // Store SHA-256 hash in database
  const keyHash = crypto
    .createHash('sha256')
    .update(apiKey)
    .digest('hex');
  
  const prefix = apiKey.substring(0, 8); // Store prefix for display
  
  await prisma.apiKey.create({
    data: {
      user_id: userId,
      key_hash: keyHash,
      prefix,
      name,
      is_active: true
    }
  });
  
  // Return plain text key ONCE (never stored)
  return apiKey;
}

// Validate API key middleware
async function requireApiKey(req: AuthRequest, res: Response, next: NextFunction) {
  try {
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ error: 'API key required' });
    }
    
    const apiKey = authHeader.substring(7);
    
    // Hash the provided key
    const keyHash = crypto
      .createHash('sha256')
      .update(apiKey)
      .digest('hex');
    
    // Find matching key
    const key = await prisma.apiKey.findUnique({
      where: { key_hash: keyHash },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            status: true
          }
        }
      }
    });
    
    if (!key || !key.is_active) {
      return res.status(401).json({ error: 'Invalid API key' });
    }
    
    if (key.user.status !== 'active') {
      return res.status(403).json({ error: 'Account suspended' });
    }
    
    // Update last_used_at
    await prisma.apiKey.update({
      where: { id: key.id },
      data: { last_used_at: new Date() }
    });
    
    // Attach user to request
    req.user = {
      userId: key.user.id,
      email: key.user.email
    };
    
    next();
  } catch (error) {
    res.status(500).json({ error: 'Authentication failed' });
  }
}
```

### 3. Password Hashing (bcrypt)

```typescript
import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12; // Recommended for production

// Hash password during registration
async function hashPassword(plainPassword: string): Promise<string> {
  return bcrypt.hash(plainPassword, SALT_ROUNDS);
}

// Verify password during login
async function verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {
  return bcrypt.compare(plainPassword, hashedPassword);
}

// Registration endpoint
router.post('/auth/register', async (req, res) => {
  const { email, password } = req.body;
  
  // Password strength validation
  if (password.length < 8) {
    return res.status(400).json({ error: 'Password must be at least 8 characters' });
  }
  
  // Check if email exists
  const existingUser = await prisma.user.findUnique({
    where: { email }
  });
  
  if (existingUser) {
    return res.status(409).json({ error: 'Email already registered' });
  }
  
  // Hash password
  const passwordHash = await hashPassword(password);
  
  // Create user
  const user = await prisma.user.create({
    data: {
      email,
      password_hash: passwordHash
    }
  });
  
  res.status(201).json({
    id: user.id,
    email: user.email
  });
});

// Login endpoint
router.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await prisma.user.findUnique({
    where: { email }
  });
  
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  const isValid = await verifyPassword(password, user.password_hash);
  
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  if (user.status !== 'active') {
    return res.status(403).json({ error: 'Account suspended' });
  }
  
  // Generate tokens
  const tokens = generateTokens({ userId: user.id, email: user.email });
  setAuthCookies(res, tokens.accessToken, tokens.refreshToken);
  
  res.json({
    success: true,
    user: {
      id: user.id,
      email: user.email
    }
  });
});
```

### 4. Rate Limiting

```typescript
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT || '6379')
});

// Global rate limit
const globalLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // 100 requests per minute
  message: 'Too many requests, please try again later',
  standardHeaders: true,
  legacyHeaders: false
});

// API key rate limit (per user)
const apiKeyLimiter = rateLimit({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:api:'
  }),
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 1000, // 1000 requests per hour per key
  keyGenerator: (req: AuthRequest) => req.user?.userId || req.ip,
  message: 'API rate limit exceeded',
  standardHeaders: true,
  legacyHeaders: false
});

// Auth endpoint rate limit (prevent brute force)
const authLimiter = rateLimit({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:auth:'
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 login attempts per 15 minutes
  skipSuccessfulRequests: true, // Don't count successful logins
  message: 'Too many login attempts, please try again later'
});

// Apply middleware
app.use('/api', globalLimiter);
app.use('/api', requireApiKey, apiKeyLimiter);
app.use('/auth/login', authLimiter);
```

### 5. HMAC Signature Verification (Webhooks)

```typescript
import crypto from 'crypto';

// Generate HMAC signature for outgoing webhooks
function generateWebhookSignature(payload: string, secret: string): string {
  return crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
}

// Verify HMAC signature for incoming webhooks (e.g., PayPal)
function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = generateWebhookSignature(payload, secret);
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Webhook middleware
function verifyWebhook(secret: string) {
  return (req: Request, res: Response, next: NextFunction) => {
    const signature = req.headers['x-webhook-signature'] as string;
    const timestamp = req.headers['x-webhook-timestamp'] as string;
    
    if (!signature || !timestamp) {
      return res.status(401).json({ error: 'Missing signature headers' });
    }
    
    // Verify timestamp (prevent replay attacks)
    const requestTime = parseInt(timestamp);
    const currentTime = Date.now();
    const timeDiff = Math.abs(currentTime - requestTime);
    
    if (timeDiff > 5 * 60 * 1000) { // 5 minute window
      return res.status(401).json({ error: 'Request timestamp too old' });
    }
    
    // Verify signature
    const payload = JSON.stringify(req.body);
    const isValid = verifyWebhookSignature(payload, signature, secret);
    
    if (!isValid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    next();
  };
}

// Usage
router.post(
  '/webhooks/paypal',
  verifyWebhook(process.env.PAYPAL_WEBHOOK_SECRET!),
  async (req, res) => {
    // Process PayPal webhook
    res.sendStatus(200);
  }
);
```

## Anti-Patterns (Forbidden)

| ❌ Mistake | ✅ Fix |
|-----------|--------|
| Storing API keys in plain text | Hash with SHA-256 before storing |
| Long-lived JWT access tokens (>1 hour) | Use 15-minute access tokens + refresh tokens |
| `bcrypt` rounds < 10 | Use 12 rounds minimum for production |
| No rate limiting on auth endpoints | Apply strict rate limits to prevent brute force |
| Trusting `X-Forwarded-For` header | Use `req.ip` or validated proxy settings |
| Comparing secrets with `===` | Use `crypto.timingSafeEqual()` to prevent timing attacks |
| No expiry on password reset tokens | Set 15-minute expiry on reset tokens |
| Allowing weak passwords | Enforce minimum 8 characters + complexity rules |
| Not validating JWT algorithm | Specify algorithm explicitly: `jwt.verify(token, secret, { algorithms: ['HS256'] })` |
| Storing JWT in localStorage | Use httpOnly cookies for web clients |

## Integration with Other Skills

**Often combined with:**
- `api-endpoint-creation` - Protecting endpoints with auth middleware
- `error-handling-logging` - Logging failed auth attempts
- `database-schema-design` - User, ApiKey, AuditLog tables

**Depends on:**
- `database-operations` (from WordPress skills) - Database queries via Prisma

## Environment Variables Required

```bash
# JWT Secrets (generate with: openssl rand -base64 32)
JWT_ACCESS_SECRET="32-char-random-string"
JWT_REFRESH_SECRET="different-32-char-random-string"

# Redis (for rate limiting) — container hostname in bridge network
REDIS_HOST="redis"
REDIS_PORT="6379"

# PayPal Webhook Secret
PAYPAL_WEBHOOK_SECRET="paypal-webhook-id"

# Node Environment
NODE_ENV="production"  # Affects cookie security
```

## Quick Reference

### Password Requirements
- Minimum length: 8 characters
- bcrypt rounds: 12
- No maximum length (bcrypt handles truncation)

### JWT Configuration
- **Access Token**: 15 minutes
- **Refresh Token**: 7 days
- **Algorithm**: HS256
- **Storage**: httpOnly cookies (web), secure storage (mobile)

### API Key Format
```
sk_live_{48 hex characters}
Example: sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
```

### Rate Limits
| Endpoint | Limit | Window |
|----------|-------|--------|
| Global API | 100 requests | 1 minute |
| Per API key | 1000 requests | 1 hour |
| Login attempts | 5 attempts | 15 minutes |
| Registration | 3 attempts | 1 hour |

### HTTP Status Codes
- `401 Unauthorized` - Missing/invalid credentials
- `403 Forbidden` - Valid credentials, insufficient permissions
- `429 Too Many Requests` - Rate limit exceeded

## Validation Checklist

Before completing authentication work:

- [ ] All passwords hashed with bcrypt (12+ rounds)
- [ ] API keys hashed with SHA-256 before storage
- [ ] JWT access tokens expire within 15 minutes
- [ ] Refresh tokens stored in httpOnly cookies
- [ ] Rate limiting applied to all public endpoints
- [ ] Auth endpoints have strict rate limits (5/15min)
- [ ] Webhook signatures verified with HMAC-SHA256
- [ ] Timestamp validation prevents replay attacks (5min window)
- [ ] Account status checked on every auth request
- [ ] Failed login attempts logged to AuditLog table
- [ ] API key `last_used_at` updated on each request
- [ ] No sensitive data logged (passwords, tokens, keys)
- [ ] Security headers set (`Helmet.js` middleware)
- [ ] CORS configured correctly for admin panel
- [ ] Environment variables validated on startup

## Security Headers (Helmet.js)

```typescript
import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));
```

## Common Security Vulnerabilities to Prevent

1. **SQL Injection**: Use Prisma ORM exclusively (no raw SQL)
2. **XSS**: Sanitize all user inputs, escape output
3. **CSRF**: Use SameSite cookies + CSRF tokens for state-changing operations
4. **Timing Attacks**: Use `crypto.timingSafeEqual()` for secret comparison
5. **Brute Force**: Rate limit auth endpoints aggressively
6. **Session Fixation**: Regenerate session tokens after login
7. **JWT Confusion**: Always specify algorithm in `jwt.verify()`
8. **Credential Stuffing**: Monitor for unusual login patterns
