Security patterns skill

Implements authentication, authorization, encryption, secrets management, and security hardening patterns.

by CloudAI-X·MIT license·★ 1,416 Stars on the repo·GitHub ↗

Use now

Files of Security patterns

CloudAI-X/main1 file shown
SKILL.md
Show the full text495 lines

Security Patterns

When to Load
  • Trigger: Auth flows, encryption, secrets management, CORS configuration, input validation, rate limiting
  • Skip: No security surface involved in the current task

Security Implementation Workflow

Copy this checklist and track progress:

Security Implementation Progress:
- [ ] Step 1: Choose authentication strategy
- [ ] Step 2: Implement authorization model
- [ ] Step 3: Set up password hashing
- [ ] Step 4: Configure secrets management
- [ ] Step 5: Enable encryption (transit + rest)
- [ ] Step 6: Configure CORS
- [ ] Step 7: Add rate limiting
- [ ] Step 8: Validate against anti-patterns checklist

Authentication Patterns

JWT (JSON Web Tokens)
import jwt from "jsonwebtoken";

function generateTokens(user: User) {
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET!,
    { expiresIn: "15m", algorithm: "HS256" },
  );
  const refreshToken = jwt.sign(
    { sub: user.id, tokenVersion: user.tokenVersion },
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: "7d" },
  );
  return { accessToken, refreshToken };
}

// WRONG: localStorage (XSS vulnerable) | CORRECT: httpOnly cookie for refresh, memory for access
res.cookie("refreshToken", refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: "strict",
  maxAge: 7 * 24 * 60 * 60 * 1000,
  path: "/api/auth/refresh",
});
JWT Verification Middleware
function authenticate(req: Request, res: Response, next: NextFunction) {
  const header = req.headers.authorization;
  if (!header?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing token" });
  }

  try {
    const token = header.slice(7);
    const payload = jwt.verify(token, process.env.JWT_SECRET!, {
      algorithms: ["HS256"], // pin: never let the token choose its algorithm
    }) as JwtPayload;
    req.user = { id: payload.sub, role: payload.role };
    next();
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      return res.status(401).json({ error: "Token expired" });
    }
    return res.status(401).json({ error: "Invalid token" });
  }
}
Session-Based Auth
import session from "express-session";
import { RedisStore } from "connect-redis";

app.use(
  session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET!,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict",
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
    },
  }),
);
OAuth 2.0 / OIDC Flow Summary
Authorization Code Flow with PKCE (all clients):
1. Redirect to provider: /authorize?response_type=code&client_id=...&redirect_uri=...&scope=openid email
   &state=RANDOM&nonce=RANDOM&code_challenge=...&code_challenge_method=S256
2. User authenticates, provider redirects back with ?code=AUTHORIZATION_CODE&state=... (reject if state differs)
3. Backend exchanges code for tokens (POST /token with code_verifier, plus client_secret for confidential clients)
4. Backend validates the id_token (signature, iss, aud, exp, nonce), then creates session/JWT

Public clients (SPAs, mobile): same flow without client_secret
NEVER use Implicit Flow (deprecated, tokens exposed in URL)
API Key Authentication
async function authenticateApiKey(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const apiKey = req.headers["x-api-key"] as string;
  if (!apiKey) return res.status(401).json({ error: "API key required" });

  // WRONG: Direct comparison (timing attack) | CORRECT: Hash-based lookup
  const hashedKey = crypto.createHash("sha256").update(apiKey).digest("hex");
  const keyRecord = await db.apiKey.findUnique({ where: { hash: hashedKey } });
  if (!keyRecord || keyRecord.revokedAt)
    return res.status(401).json({ error: "Invalid API key" });

  req.apiClient = { id: keyRecord.clientId, scopes: keyRecord.scopes };
  next();
}

Authorization Models

RBAC (Role-Based Access Control)
const PERMISSIONS = {
  admin: [
    "users:read",
    "users:write",
    "users:delete",
    "posts:read",
    "posts:write",
    "posts:delete",
  ],
  editor: ["posts:read", "posts:write", "posts:delete", "users:read"],
  viewer: ["posts:read", "users:read"],
} as const;

type Role = keyof typeof PERMISSIONS;

function authorize(...requiredPermissions: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const userPermissions = PERMISSIONS[req.user.role as Role] || [];
    const hasPermission = requiredPermissions.every((p) =>
      (userPermissions as readonly string[]).includes(p),
    );
    if (!hasPermission)
      return res.status(403).json({ error: "Insufficient permissions" });
    next();
  };
}

// Usage: app.delete("/api/users/:id", authenticate, authorize("users:delete"), deleteUser);
Resource-Level Authorization
// WRONG: Only checking role, not ownership -- any editor can edit ANY post
// CORRECT: Check ownership or admin role
app.put(
  "/api/posts/:id",
  authenticate,
  authorize("posts:write"),
  async (req, res) => {
    const post = await db.post.findUnique({ where: { id: req.params.id } });
    if (!post) return res.status(404).json({ error: "Not found" });
    if (post.authorId !== req.user.id && req.user.role !== "admin") {
      return res
        .status(403)
        .json({ error: "Not authorized to edit this post" });
    }
    // WRONG: data: req.body -- mass assignment lets the client overwrite authorId
    const { title, body } = UpdatePostSchema.parse(req.body); // allowlisted fields only
    const updated = await db.post.update({
      where: { id: post.id },
      data: { title, body },
    });
    res.json(updated);
  },
);

Password Handling

import bcrypt from "bcrypt";
// WRONG: plaintext or MD5/SHA256 (too fast, brute-forceable)
// CORRECT: bcrypt with appropriate cost factor
const SALT_ROUNDS = 12; // ~250ms on modern hardware

async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(
  password: string,
  hash: string,
): Promise<boolean> {
  return bcrypt.compare(password, hash); // constant-time comparison built-in
}

// Registration
await db.user.create({
  data: { email, password: await hashPassword(req.body.password) },
});

// Login -- WRONG: "Invalid password" (reveals email exists) | CORRECT: generic message
// Always run bcrypt, even for unknown emails, or response time reveals which emails exist
const user = await db.user.findUnique({ where: { email } });
const hash = user?.password ?? DUMMY_HASH; // bcrypt hash of a random string, created at startup
if (!(await verifyPassword(req.body.password, hash)) || !user) {
  return res.status(401).json({ error: "Invalid email or password" });
}
Password Policies
function validatePassword(password: string): string[] {
  const errors: string[] = [];
  if (password.length < 12) errors.push("Minimum 12 characters");
  if (Buffer.byteLength(password) > 72) errors.push("Maximum 72 bytes"); // bcrypt ignores the rest

  // Check against breached password lists (haveibeenpwned API or local)
  // Do NOT enforce arbitrary complexity rules (uppercase + number + symbol)
  // NIST 800-63B recommends length over complexity
  return errors;
}

Secrets Management

# WRONG: Hardcoded values in source code
# API_KEY = "some-value-here"

# CORRECT: Environment variables loaded from .env
from dotenv import load_dotenv
import os

load_dotenv()
api_key = os.getenv("API_KEY")
db_url = os.getenv("DATABASE_URL")

# CORRECT: Secrets manager for production
# AWS: Secrets Manager, Parameter Store
# GCP: Secret Manager
# HashiCorp Vault for self-hosted
Secret Rotation
1. Generate new secret value
2. Deploy code that accepts BOTH old and new values
3. Update all consumers to use the new value
4. Verify old value is no longer in use
5. Revoke old value

Never: Rotate in-place without a transition period

Encryption Patterns

In Transit
// Redirect HTTP to HTTPS in production
app.use((req, res, next) => {
  if (
    req.headers["x-forwarded-proto"] !== "https" &&
    process.env.NODE_ENV === "production"
  ) {
    return res.redirect(301, `https://${req.hostname}${req.url}`);
  }
  next();
});
// HSTS header
app.use((req, res, next) => {
  res.setHeader(
    "Strict-Transport-Security",
    "max-age=31536000; includeSubDomains",
  );
  next();
});
At Rest
import crypto from "crypto";
const ALGORITHM = "aes-256-gcm";

function encrypt(
  plaintext: string,
  key: Buffer,
): { ciphertext: string; iv: string; tag: string } {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
  let ciphertext =
    cipher.update(plaintext, "utf8", "hex") + cipher.final("hex");
  return {
    ciphertext,
    iv: iv.toString("hex"),
    tag: cipher.getAuthTag().toString("hex"),
  };
}

function decrypt(
  ciphertext: string,
  key: Buffer,
  iv: string,
  tag: string,
): string {
  const decipher = crypto.createDecipheriv(
    ALGORITHM,
    key,
    Buffer.from(iv, "hex"),
  );
  decipher.setAuthTag(Buffer.from(tag, "hex"));
  return decipher.update(ciphertext, "hex", "utf8") + decipher.final("utf8");
}
// Use for PII, sensitive data. Encryption key in secrets manager, NOT in code.

CORS Configuration

import cors from "cors";

// WRONG: Allow everything
app.use(cors()); // origin: *, credentials: false

// WRONG: Wildcard with credentials
app.use(cors({ origin: "*", credentials: true })); // browsers reject this

// CORRECT: Explicit allowed origins
const ALLOWED_ORIGINS = [
  "https://myapp.com",
  "https://admin.myapp.com",
  ...(process.env.NODE_ENV !== "production" ? ["http://localhost:3000"] : []),
];

app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || ALLOWED_ORIGINS.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("Not allowed by CORS"));
      }
    },
    credentials: true,
    methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
    allowedHeaders: ["Content-Type", "Authorization"],
    maxAge: 86400, // cache preflight for 24 hours
  }),
);

Rate Limiting

import rateLimit, { ipKeyGenerator } from "express-rate-limit";
import RedisStore from "rate-limit-redis";

// Global rate limit
app.use(
  rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    limit: 100, // 100 requests per window
    standardHeaders: true, // RateLimit-* headers
    legacyHeaders: false,
    store: new RedisStore({
      sendCommand: (...args) => redisClient.sendCommand(args),
    }),
  }),
);

// Strict limit on auth endpoints
app.use(
  "/api/auth/login",
  rateLimit({
    windowMs: 15 * 60 * 1000,
    limit: 5, // 5 login attempts per 15 min
    message: { error: "Too many login attempts. Try again later." },
  }),
);

// Per-API-key rate limiting for developer APIs
app.use(
  "/api/v1/",
  rateLimit({
    windowMs: 60 * 1000, // 1 minute
    limit: 60, // 60 requests per minute
    keyGenerator: (req) => req.apiClient?.id ?? ipKeyGenerator(req.ip),
  }),
);

Security Headers

import helmet from "helmet";

app.use(helmet()); // Sets many secure headers at once

// Key headers helmet sets:
// X-Content-Type-Options: nosniff
// X-Frame-Options: SAMEORIGIN
// Strict-Transport-Security: max-age=31536000; includeSubDomains
// Content-Security-Policy: default-src 'self'

// Customize CSP for your app
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https://cdn.example.com"],
      connectSrc: ["'self'", "https://api.example.com"],
    },
  }),
);

Input Validation

import { z } from "zod";

// WRONG: Trusting user input directly (SQL injection risk)
app.post("/api/users", (req, res) => {
  db.query(`SELECT * FROM users WHERE email = '${req.body.email}'`);
});

// CORRECT: Validate with schema, use parameterized queries
const CreateUserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().trim().min(1).max(100),
  age: z.number().int().min(13).max(150).optional(),
});

app.post("/api/users", async (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() });
  }
  // Use parameterized query (ORM or prepared statement)
  await db.user.create({ data: result.data });
});

Common Anti-Patterns Summary

AVOID                              DO INSTEAD
-------------------------------------------------------------------
JWT in localStorage                httpOnly secure cookie (refresh), memory (access)
MD5/SHA for passwords              bcrypt or argon2 with proper cost factor
Hardcoded secrets in code          Environment variables + secrets manager
cors({ origin: '*' })             Explicit allowed origins list
"Invalid password" message         "Invalid email or password" (no enumeration)
No rate limiting on auth           Strict rate limits on login/register
Rolling your own crypto            Use established libraries (jose, bcrypt)
Trusting user input                Validate with zod/joi, parameterized queries
Same API key forever               Rotate keys regularly, support multiple active
No HTTPS redirect                  Force HTTPS + HSTS header
Symmetric JWT for multi-service    Use RS256/ES256 (asymmetric) for distributed
No input length limits             Max length on all string inputs
1---
2name: security-patterns
3description: Implements authentication, authorization, encryption, secrets management, and security hardening patterns. Use when designing auth flows, managing secrets, configuring CORS, implementing rate limiting, or when asked about JWT, OAuth, password hashing, API keys, RBAC, or security best practices.
4---
5 
6# Security Patterns
7 
8### When to Load
9 
10- **Trigger**: Auth flows, encryption, secrets management, CORS configuration, input validation, rate limiting
11- **Skip**: No security surface involved in the current task
12 
13## Security Implementation Workflow
14 
15Copy this checklist and track progress:
16 
17```
18Security Implementation Progress:
19- [ ] Step 1: Choose authentication strategy
20- [ ] Step 2: Implement authorization model
21- [ ] Step 3: Set up password hashing
22- [ ] Step 4: Configure secrets management
23- [ ] Step 5: Enable encryption (transit + rest)
24- [ ] Step 6: Configure CORS
25- [ ] Step 7: Add rate limiting
26- [ ] Step 8: Validate against anti-patterns checklist
27```
28 
29## Authentication Patterns
30 
31### JWT (JSON Web Tokens)
32 
33```typescript
34import jwt from "jsonwebtoken";
35 
36function generateTokens(user: User) {
37 const accessToken = jwt.sign(
38 { sub: user.id, role: user.role },
39 process.env.JWT_SECRET!,
40 { expiresIn: "15m", algorithm: "HS256" },
41 );
42 const refreshToken = jwt.sign(
43 { sub: user.id, tokenVersion: user.tokenVersion },
44 process.env.JWT_REFRESH_SECRET!,
45 { expiresIn: "7d" },
46 );
47 return { accessToken, refreshToken };
48}
49 
50// WRONG: localStorage (XSS vulnerable) | CORRECT: httpOnly cookie for refresh, memory for access
51res.cookie("refreshToken", refreshToken, {
52 httpOnly: true,
53 secure: true,
54 sameSite: "strict",
55 maxAge: 7 * 24 * 60 * 60 * 1000,
56 path: "/api/auth/refresh",
57});
58```
59 
60### JWT Verification Middleware
61 
62```typescript
63function authenticate(req: Request, res: Response, next: NextFunction) {
64 const header = req.headers.authorization;
65 if (!header?.startsWith("Bearer ")) {
66 return res.status(401).json({ error: "Missing token" });
67 }
68 
69 try {
70 const token = header.slice(7);
71 const payload = jwt.verify(token, process.env.JWT_SECRET!, {
72 algorithms: ["HS256"], // pin: never let the token choose its algorithm
73 }) as JwtPayload;
74 req.user = { id: payload.sub, role: payload.role };
75 next();
76 } catch (err) {
77 if (err instanceof jwt.TokenExpiredError) {
78 return res.status(401).json({ error: "Token expired" });
79 }
80 return res.status(401).json({ error: "Invalid token" });
81 }
82}
83```
84 
85### Session-Based Auth
86 
87```typescript
88import session from "express-session";
89import { RedisStore } from "connect-redis";
90 
91app.use(
92 session({
93 store: new RedisStore({ client: redisClient }),
94 secret: process.env.SESSION_SECRET!,
95 resave: false,
96 saveUninitialized: false,
97 cookie: {
98 httpOnly: true,
99 secure: process.env.NODE_ENV === "production",
100 sameSite: "strict",
101 maxAge: 24 * 60 * 60 * 1000, // 24 hours
102 },
103 }),
104);
105```
106 
107### OAuth 2.0 / OIDC Flow Summary
108 
109```
110Authorization Code Flow with PKCE (all clients):
1111. Redirect to provider: /authorize?response_type=code&client_id=...&redirect_uri=...&scope=openid email
112 &state=RANDOM&nonce=RANDOM&code_challenge=...&code_challenge_method=S256
1132. User authenticates, provider redirects back with ?code=AUTHORIZATION_CODE&state=... (reject if state differs)
1143. Backend exchanges code for tokens (POST /token with code_verifier, plus client_secret for confidential clients)
1154. Backend validates the id_token (signature, iss, aud, exp, nonce), then creates session/JWT
116 
117Public clients (SPAs, mobile): same flow without client_secret
118NEVER use Implicit Flow (deprecated, tokens exposed in URL)
119```
120 
121### API Key Authentication
122 
123```typescript
124async function authenticateApiKey(
125 req: Request,
126 res: Response,
127 next: NextFunction,
128) {
129 const apiKey = req.headers["x-api-key"] as string;
130 if (!apiKey) return res.status(401).json({ error: "API key required" });
131 
132 // WRONG: Direct comparison (timing attack) | CORRECT: Hash-based lookup
133 const hashedKey = crypto.createHash("sha256").update(apiKey).digest("hex");
134 const keyRecord = await db.apiKey.findUnique({ where: { hash: hashedKey } });
135 if (!keyRecord || keyRecord.revokedAt)
136 return res.status(401).json({ error: "Invalid API key" });
137 
138 req.apiClient = { id: keyRecord.clientId, scopes: keyRecord.scopes };
139 next();
140}
141```
142 
143## Authorization Models
144 
145### RBAC (Role-Based Access Control)
146 
147```typescript
148const PERMISSIONS = {
149 admin: [
150 "users:read",
151 "users:write",
152 "users:delete",
153 "posts:read",
154 "posts:write",
155 "posts:delete",
156 ],
157 editor: ["posts:read", "posts:write", "posts:delete", "users:read"],
158 viewer: ["posts:read", "users:read"],
159} as const;
160 
161type Role = keyof typeof PERMISSIONS;
162 
163function authorize(...requiredPermissions: string[]) {
164 return (req: Request, res: Response, next: NextFunction) => {
165 const userPermissions = PERMISSIONS[req.user.role as Role] || [];
166 const hasPermission = requiredPermissions.every((p) =>
167 (userPermissions as readonly string[]).includes(p),
168 );
169 if (!hasPermission)
170 return res.status(403).json({ error: "Insufficient permissions" });
171 next();
172 };
173}
174 
175// Usage: app.delete("/api/users/:id", authenticate, authorize("users:delete"), deleteUser);
176```
177 
178### Resource-Level Authorization
179 
180```typescript
181// WRONG: Only checking role, not ownership -- any editor can edit ANY post
182// CORRECT: Check ownership or admin role
183app.put(
184 "/api/posts/:id",
185 authenticate,
186 authorize("posts:write"),
187 async (req, res) => {
188 const post = await db.post.findUnique({ where: { id: req.params.id } });
189 if (!post) return res.status(404).json({ error: "Not found" });
190 if (post.authorId !== req.user.id && req.user.role !== "admin") {
191 return res
192 .status(403)
193 .json({ error: "Not authorized to edit this post" });
194 }
195 // WRONG: data: req.body -- mass assignment lets the client overwrite authorId
196 const { title, body } = UpdatePostSchema.parse(req.body); // allowlisted fields only
197 const updated = await db.post.update({
198 where: { id: post.id },
199 data: { title, body },
200 });
201 res.json(updated);
202 },
203);
204```
205 
206## Password Handling
207 
208```typescript
209import bcrypt from "bcrypt";
210// WRONG: plaintext or MD5/SHA256 (too fast, brute-forceable)
211// CORRECT: bcrypt with appropriate cost factor
212const SALT_ROUNDS = 12; // ~250ms on modern hardware
213 
214async function hashPassword(password: string): Promise<string> {
215 return bcrypt.hash(password, SALT_ROUNDS);
216}
217async function verifyPassword(
218 password: string,
219 hash: string,
220): Promise<boolean> {
221 return bcrypt.compare(password, hash); // constant-time comparison built-in
222}
223 
224// Registration
225await db.user.create({
226 data: { email, password: await hashPassword(req.body.password) },
227});
228 
229// Login -- WRONG: "Invalid password" (reveals email exists) | CORRECT: generic message
230// Always run bcrypt, even for unknown emails, or response time reveals which emails exist
231const user = await db.user.findUnique({ where: { email } });
232const hash = user?.password ?? DUMMY_HASH; // bcrypt hash of a random string, created at startup
233if (!(await verifyPassword(req.body.password, hash)) || !user) {
234 return res.status(401).json({ error: "Invalid email or password" });
235}
236```
237 
238### Password Policies
239 
240```typescript
241function validatePassword(password: string): string[] {
242 const errors: string[] = [];
243 if (password.length < 12) errors.push("Minimum 12 characters");
244 if (Buffer.byteLength(password) > 72) errors.push("Maximum 72 bytes"); // bcrypt ignores the rest
245 
246 // Check against breached password lists (haveibeenpwned API or local)
247 // Do NOT enforce arbitrary complexity rules (uppercase + number + symbol)
248 // NIST 800-63B recommends length over complexity
249 return errors;
250}
251```
252 
253## Secrets Management
254 
255```python
256# WRONG: Hardcoded values in source code
257# API_KEY = "some-value-here"
258 
259# CORRECT: Environment variables loaded from .env
260from dotenv import load_dotenv
261import os
262 
263load_dotenv()
264api_key = os.getenv("API_KEY")
265db_url = os.getenv("DATABASE_URL")
266 
267# CORRECT: Secrets manager for production
268# AWS: Secrets Manager, Parameter Store
269# GCP: Secret Manager
270# HashiCorp Vault for self-hosted
271```
272 
273### Secret Rotation
274 
275```
2761. Generate new secret value
2772. Deploy code that accepts BOTH old and new values
2783. Update all consumers to use the new value
2794. Verify old value is no longer in use
2805. Revoke old value
281 
282Never: Rotate in-place without a transition period
283```
284 
285## Encryption Patterns
286 
287### In Transit
288 
289```typescript
290// Redirect HTTP to HTTPS in production
291app.use((req, res, next) => {
292 if (
293 req.headers["x-forwarded-proto"] !== "https" &&
294 process.env.NODE_ENV === "production"
295 ) {
296 return res.redirect(301, `https://${req.hostname}${req.url}`);
297 }
298 next();
299});
300// HSTS header
301app.use((req, res, next) => {
302 res.setHeader(
303 "Strict-Transport-Security",
304 "max-age=31536000; includeSubDomains",
305 );
306 next();
307});
308```
309 
310### At Rest
311 
312```typescript
313import crypto from "crypto";
314const ALGORITHM = "aes-256-gcm";
315 
316function encrypt(
317 plaintext: string,
318 key: Buffer,
319): { ciphertext: string; iv: string; tag: string } {
320 const iv = crypto.randomBytes(16);
321 const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
322 let ciphertext =
323 cipher.update(plaintext, "utf8", "hex") + cipher.final("hex");
324 return {
325 ciphertext,
326 iv: iv.toString("hex"),
327 tag: cipher.getAuthTag().toString("hex"),
328 };
329}
330 
331function decrypt(
332 ciphertext: string,
333 key: Buffer,
334 iv: string,
335 tag: string,
336): string {
337 const decipher = crypto.createDecipheriv(
338 ALGORITHM,
339 key,
340 Buffer.from(iv, "hex"),
341 );
342 decipher.setAuthTag(Buffer.from(tag, "hex"));
343 return decipher.update(ciphertext, "hex", "utf8") + decipher.final("utf8");
344}
345// Use for PII, sensitive data. Encryption key in secrets manager, NOT in code.
346```
347 
348## CORS Configuration
349 
350```typescript
351import cors from "cors";
352 
353// WRONG: Allow everything
354app.use(cors()); // origin: *, credentials: false
355 
356// WRONG: Wildcard with credentials
357app.use(cors({ origin: "*", credentials: true })); // browsers reject this
358 
359// CORRECT: Explicit allowed origins
360const ALLOWED_ORIGINS = [
361 "https://myapp.com",
362 "https://admin.myapp.com",
363 ...(process.env.NODE_ENV !== "production" ? ["http://localhost:3000"] : []),
364];
365 
366app.use(
367 cors({
368 origin: (origin, callback) => {
369 if (!origin || ALLOWED_ORIGINS.includes(origin)) {
370 callback(null, true);
371 } else {
372 callback(new Error("Not allowed by CORS"));
373 }
374 },
375 credentials: true,
376 methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
377 allowedHeaders: ["Content-Type", "Authorization"],
378 maxAge: 86400, // cache preflight for 24 hours
379 }),
380);
381```
382 
383## Rate Limiting
384 
385```typescript
386import rateLimit, { ipKeyGenerator } from "express-rate-limit";
387import RedisStore from "rate-limit-redis";
388 
389// Global rate limit
390app.use(
391 rateLimit({
392 windowMs: 15 * 60 * 1000, // 15 minutes
393 limit: 100, // 100 requests per window
394 standardHeaders: true, // RateLimit-* headers
395 legacyHeaders: false,
396 store: new RedisStore({
397 sendCommand: (...args) => redisClient.sendCommand(args),
398 }),
399 }),
400);
401 
402// Strict limit on auth endpoints
403app.use(
404 "/api/auth/login",
405 rateLimit({
406 windowMs: 15 * 60 * 1000,
407 limit: 5, // 5 login attempts per 15 min
408 message: { error: "Too many login attempts. Try again later." },
409 }),
410);
411 
412// Per-API-key rate limiting for developer APIs
413app.use(
414 "/api/v1/",
415 rateLimit({
416 windowMs: 60 * 1000, // 1 minute
417 limit: 60, // 60 requests per minute
418 keyGenerator: (req) => req.apiClient?.id ?? ipKeyGenerator(req.ip),
419 }),
420);
421```
422 
423## Security Headers
424 
425```typescript
426import helmet from "helmet";
427 
428app.use(helmet()); // Sets many secure headers at once
429 
430// Key headers helmet sets:
431// X-Content-Type-Options: nosniff
432// X-Frame-Options: SAMEORIGIN
433// Strict-Transport-Security: max-age=31536000; includeSubDomains
434// Content-Security-Policy: default-src 'self'
435 
436// Customize CSP for your app
437app.use(
438 helmet.contentSecurityPolicy({
439 directives: {
440 defaultSrc: ["'self'"],
441 scriptSrc: ["'self'"],
442 styleSrc: ["'self'", "'unsafe-inline'"],
443 imgSrc: ["'self'", "data:", "https://cdn.example.com"],
444 connectSrc: ["'self'", "https://api.example.com"],
445 },
446 }),
447);
448```
449 
450## Input Validation
451 
452```typescript
453import { z } from "zod";
454 
455// WRONG: Trusting user input directly (SQL injection risk)
456app.post("/api/users", (req, res) => {
457 db.query(`SELECT * FROM users WHERE email = '${req.body.email}'`);
458});
459 
460// CORRECT: Validate with schema, use parameterized queries
461const CreateUserSchema = z.object({
462 email: z.string().email().max(255),
463 name: z.string().trim().min(1).max(100),
464 age: z.number().int().min(13).max(150).optional(),
465});
466 
467app.post("/api/users", async (req, res) => {
468 const result = CreateUserSchema.safeParse(req.body);
469 if (!result.success) {
470 return res.status(400).json({ errors: result.error.flatten() });
471 }
472 // Use parameterized query (ORM or prepared statement)
473 await db.user.create({ data: result.data });
474});
475```
476 
477## Common Anti-Patterns Summary
478 
479```
480AVOID DO INSTEAD
481-------------------------------------------------------------------
482JWT in localStorage httpOnly secure cookie (refresh), memory (access)
483MD5/SHA for passwords bcrypt or argon2 with proper cost factor
484Hardcoded secrets in code Environment variables + secrets manager
485cors({ origin: '*' }) Explicit allowed origins list
486"Invalid password" message "Invalid email or password" (no enumeration)
487No rate limiting on auth Strict rate limits on login/register
488Rolling your own crypto Use established libraries (jose, bcrypt)
489Trusting user input Validate with zod/joi, parameterized queries
490Same API key forever Rotate keys regularly, support multiple active
491No HTTPS redirect Force HTTPS + HSTS header
492Symmetric JWT for multi-service Use RS256/ES256 (asymmetric) for distributed
493No input length limits Max length on all string inputs
494```
495 

Discussion

Alternatives

API and interface designGuides stable API and interface design. Use when designing APIs, module boundaries, or any public interface. Use when creating REST or GraphQL endpoints, defining type contracts between modules, or establishing boundaries between frontend and backend.Coding · MITContext7Pulls up-to-date, version-specific library docs and code examples into the prompt so the AI stops inventing old APIs.Coding · MITContext7 Documentation LookupFetch up-to-date documentation and code examples for any library, framework, SDK, CLI tool, or cloud service. Use whenever the user asks about a specific library — even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot — because training data may not reflect recent API changes or version updates. Always use for: API syntax questions, configuration options, version migration issues, "how do I" questions mentioning a library name, debugging that involves library-specific behavior, setup instructions, and CLI tool usage. Use even when you think you know the answer. Do not rely on training data for API details, signatures, or configuration options — they are frequently out of date. Prefer this over web search for library documentation.Coding · MITAdaptyv Bio Foundry APIHow to use the Adaptyv Bio Foundry API and Python SDK for protein experiment design, submission, and results retrieval. Use this skill whenever the user mentions Adaptyv, Foundry API, protein binding assays, protein screening experiments, BLI/SPR assays, thermostability assays, or wants to submit protein sequences for experimental characterization. Also trigger when code imports `adaptyv`, `adaptyv_sdk`, or `FoundryClient`, or references `foundry-api-public.adaptyvbio.com`.Science · MIT