I create modern web applications and custom digital tools to help businesses grow through technological innovation. My passion is combining computer science and economics to generate real value.
My passion for computer science was born at the Technical Commercial Institute of Maglie, where I discovered the power of programming and the fascination of creating digital solutions. From the start, I understood that computer science was not just code, but an extraordinary tool for turning ideas into reality.
During my studies in Business Information Systems, I began to interweave computer science and economics, understanding how technology can be the engine of growth for any business. This vision accompanied me to the University of Bari, where I obtained my degree in Computer Science, deepening my technical skills and passion for software development.
Today I put this experience at the service of businesses, professionals and startups, creating tailor-made digital solutions that automate processes, optimize resources and open new business opportunities. Because true innovation begins when technology meets the real needs of people.
My Skills
Data Analysis & Predictive Models
I transform data into strategic insights with in-depth analysis and predictive models for informed decisions
Process Automation
I create custom tools that automate repetitive operations and free up time for value-added activities
Custom Systems
I develop tailor-made software systems, from platform integrations to customized dashboards
Credo fermamente che l'informatica sia lo strumento più potente per trasformare le idee in realtà e migliorare la vita delle persone.
Democratizzare la Tecnologia
La mia missione è rendere l'informatica accessibile a tutti: dalle piccole imprese locali alle startup innovative, fino ai professionisti che vogliono digitalizzare la propria attività. Ogni realtà merita di sfruttare le potenzialità del digitale.
Unire Informatica ed Economia
Non è solo questione di scrivere codice: è capire come la tecnologia possa generare valore reale. Intrecciando competenze informatiche e visione economica, aiuto le attività a crescere, ottimizzare processi e raggiungere nuovi traguardi di efficienza e redditività.
Creare Soluzioni su Misura
Ogni attività è unica, e così devono esserlo le soluzioni. Sviluppo strumenti personalizzati che rispondono alle esigenze specifiche di ciascun cliente, automatizzando processi ripetitivi e liberando tempo per ciò che conta davvero: far crescere il business.
Trasforma la Tua Attività con la Tecnologia
Che tu gestisca un negozio, uno studio professionale o un'azienda, posso aiutarti a sfruttare le potenzialità dell'informatica per lavorare meglio, più velocemente e in modo più intelligente.
Bari, Puglia, Italy · Hybrid
Analysis and development of computer systems through the use of Java and Quarkus in Health and Public Sector. Continuous training on modern technologies for creating customized and efficient software solutions and on agents.
💼
06/2022 - 12/2024
Software analyst and Back End Developer Associate Consultant
Links Management and Technology SpA
Experience analyzing as-is software systems and ETL flows using PowerCenter. Completed Spring Boot training for developing modern and scalable backend applications. Backend developer specialized in Spring Boot, with experience in database design, analysis, development and testing of assigned tasks.
💼
02/2021 - 10/2021
Software programmer
Adesso.it (prima era WebScience srl)
Experience in AS-IS and TO-BE analysis, SEO evolutions and website evolutions to improve user performance and engagement.
🎓
2018 - 2025
Degree in Computer Science
University of Bari Aldo Moro
Bachelor's degree in Computer Science, focusing on software engineering, algorithms, and modern development practices.
📚
2013 - 2018
Diploma - Corporate Information Systems
Technical Commercial Institute of Maglie
Technical diploma specializing in Business Information Systems, combining IT knowledge with business management.
Contattami
Hai un progetto in mente? Parliamone! Compila il form qui sotto e ti risponderò al più presto.
* Campi obbligatori. I tuoi dati saranno utilizzati solo per rispondere alla tua richiesta.
05 - API Security: OAuth 2.1, JWT Best Practices and Rate Limiting
APIs are the backbone of modern applications. Every microservice, every mobile app, every SaaS
integration runs through HTTP endpoints that expose critical data and functionality. According to
the Salt Security State of API Security 2024 report, API attacks increased by
167% over the previous 12 months, with 94% of organizations experiencing at least
one API-related security incident during the year. These are not abstract statistics: the vast
majority of these vulnerabilities stem from patterns developers replicate every day.
The problem is structural. APIs get designed with functionality in mind, and security gets bolted
on as an afterthought. A JWT token gets added and the job is considered done. But the OWASP API
Security Top 10:2023 shows that the most critical attack vectors are not technical — they are
logical. Broken Object Level Authorization, Broken Function Level Authorization, Unrestricted
Resource Consumption. Vulnerabilities that no framework resolves automatically, because they
require deliberate architectural decisions.
This article walks you through the entire attack surface of modern APIs: from the OWASP API
Top 10:2023 to rate limiting with token bucket and sliding window algorithms, from OAuth 2.1
with granular scopes to input validation, from correct CORS configuration to API gateway patterns.
Practical Node.js/Express code for every section, plus an Angular-specific security checklist.
What You Will Learn
OWASP API Security Top 10:2023: the 10 most critical vulnerabilities with practical examples
Rate limiting: implementing token bucket and sliding window algorithms with Redis
OAuth 2.1 with granular scopes: key differences from OAuth 2.0 and mandatory PKCE
JWT best practices: secure algorithms, token rotation, revocation and blacklisting
API keys vs Bearer tokens: when to use each approach and how to manage them securely
Input validation and sanitization with Zod for TypeScript
Secure CORS configuration: origin whitelisting and credential handling
API gateway patterns: centralized authentication, circuit breaker, secure logging
Monitoring and alerting: detecting attack patterns in real time
Angular security checklist for API calls from the frontend
OWASP API Security Top 10:2023
The OWASP API Security Top 10:2023 is the reference list for understanding the most critical
risks in modern APIs. Compared to the 2019 edition, it introduces three new categories and
re-prioritizes based on real incident data. This is not a theoretical list: every entry
corresponds to documented vulnerabilities that caused real breaches in recent years.
BOLA has been number one for three consecutive editions and accounts for roughly 40% of all
documented API attacks. The API returns resources identified by an ID without verifying that
the authenticated user has rights to that specific resource. It is the API version of the classic
IDOR (Insecure Direct Object Reference): the client requests /api/invoices/1234 and
the server responds without checking whether invoice 1234 belongs to the requesting user.
// VULNERABLE: Any authenticated user can read any customer's invoice
app.get('/api/invoices/:id', authenticate, async (req, res) => {
// Missing ownership check: req.user.id vs invoice.customerId
const invoice = await Invoice.findById(req.params.id);
res.json(invoice);
});
// CORRECT: Always verify the resource belongs to the requesting user
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findOne({
_id: req.params.id,
customerId: req.user.id, // Ownership filter in the query itself
});
if (!invoice) {
// Return 404, not 403 — do not reveal the resource exists
return res.status(404).json({ error: 'Invoice not found' });
}
res.json(invoice);
});
// With TypeORM: BOLA-safe query builder
const invoice = await invoiceRepository.findOne({
where: {
id: parseInt(req.params.id),
customer: { id: req.user.id }, // Implicit JOIN with ownership check
},
});
// Reusable helper for systematic BOLA prevention
async function findOwnedResource<T>(
model: Model<T>,
resourceId: string,
ownerId: string,
ownerField = 'userId'
): Promise<T | null> {
return model.findOne({
_id: resourceId,
[ownerField]: ownerId,
});
}
API2:2023 - Broken Authentication
Broken authentication goes beyond simply missing a login requirement. It includes JWT tokens
with algorithm none, weak signing keys, missing validation of the audience
(aud) and issuer (iss) claims, tokens that never expire, and lack
of brute-force protection on login endpoints. An attacker who finds a JWT signed with
HS256 and a weak key like secret can reassign themselves any role.
New in 2023, BOPLA merges two previous vulnerabilities: Excessive Data Exposure
(the API returns more fields than necessary, including sensitive data) and Mass Assignment
(the API accepts fields it should not, allowing users to set isAdmin or
role). The secure pattern is explicit projection of allowed fields in both input
and output.
// VULNERABLE: Mass Assignment — user can set isAdmin flag
app.put('/api/users/:id', authenticate, async (req, res) => {
// req.body may contain { name: 'John', isAdmin: true, role: 'superadmin' }
await User.findByIdAndUpdate(req.params.id, req.body); // Accepts everything
});
// CORRECT: Explicit field whitelist with Zod
import { z } from 'zod';
const UpdateUserSchema = z.object({
name: z.string().min(2).max(100).optional(),
email: z.string().email().optional(),
bio: z.string().max(500).optional(),
// isAdmin, role, permissions: NOT in the schema = not accepted
});
app.put('/api/users/:id', authenticate, async (req, res) => {
const parsed = UpdateUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
await User.findByIdAndUpdate(req.params.id, parsed.data);
res.json({ success: true });
});
// VULNERABLE: Excessive Data Exposure
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // Returns passwordHash, twoFactorSecret, internalNotes...
});
// CORRECT: Explicit projection — public fields only
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id)
.select('name email avatar bio createdAt -_id');
res.json(user);
});
API4:2023 - Unrestricted Resource Consumption
Replacing "Lack of Resources and Rate Limiting", this category covers all scenarios where an API
does not limit resource consumption: requests without pagination returning millions of records,
file uploads without size limits, GraphQL queries with unlimited depth, webhooks without timeouts,
and CPU-intensive operations without throttling. Rate limiting alone is not enough: limits must
be applied at every level of the pipeline.
API5 - API10: Other Critical Vulnerabilities
API5 - Broken Function Level Authorization: Admin endpoints accessible to regular users (often documented but not actually protected by the code)
API6 - Unrestricted Access to Sensitive Business Flows: Abuse of legitimate flows such as bulk account creation, automated purchases, or OTP sending at scale
API7 - Server Side Request Forgery (SSRF): The API accepts client-supplied URLs and fetches them server-side, enabling access to internal services
API8 - Security Misconfiguration: Missing headers, CORS wildcards, debug mode in production, deprecated API versions exposed without authentication
API9 - Improper Inventory Management: Deprecated API versions not removed, test endpoints in production, undocumented shadow APIs
API10 - Unsafe Consumption of APIs: Blind trust in third-party API responses without output validation and proper error handling
Rate Limiting: Algorithms and Implementation
Rate limiting is the mechanism that prevents API abuse by capping the number of requests a client
can make within a time window. It is not just a DDoS mitigation measure: it also protects against
credential stuffing, scraping, resource enumeration, and business flow abuse. The choice of
algorithm impacts user experience and effective protection.
Token Bucket Algorithm
The token bucket is the most common rate limiting algorithm. Each client has a "bucket" with a
maximum capacity of N tokens. Tokens are added at a constant rate of R tokens per second. Each
request consumes one token. If the bucket is empty, the request is rejected with HTTP 429.
The key advantage is that it allows traffic bursts up to the bucket capacity,
then stabilizes at R requests per second. Ideal for public APIs with naturally bursty traffic.
// Token Bucket with Redis — production-ready implementation
import Redis from 'ioredis';
import { Request, Response, NextFunction } from 'express';
const redis = new Redis(process.env.REDIS_URL!);
interface TokenBucketConfig {
capacity: number; // Max bucket size (max burst)
refillRate: number; // Tokens added per second (steady state)
}
class TokenBucketLimiter {
constructor(private config: TokenBucketConfig) {}
async checkLimit(key: string): Promise<{
allowed: boolean;
remaining: number;
resetMs: number;
}> {
const now = Date.now();
const bucketKey = `rl:tb:
#123;key}`;
// Lua script for atomic operation — prevents race conditions
const luaScript = `
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1]) or capacity
local last_ts = tonumber(data[2]) or now
local elapsed_sec = (now - last_ts) / 1000.0
local refilled = math.min(capacity, tokens + elapsed_sec * refill_rate)
if refilled >= 1.0 then
local remaining = refilled - 1.0
redis.call('HMSET', KEYS[1], 'tokens', remaining, 'ts', now)
redis.call('PEXPIRE', KEYS[1], 3600000)
return {1, math.floor(remaining)}
else
redis.call('HMSET', KEYS[1], 'tokens', refilled, 'ts', now)
redis.call('PEXPIRE', KEYS[1], 3600000)
return {0, 0}
end
`;
const result = await redis.eval(
luaScript, 1, bucketKey,
this.config.capacity,
this.config.refillRate,
now
) as [number, number];
const resetMs = result[0] === 0
? Math.ceil((1 / this.config.refillRate) * 1000)
: 0;
return {
allowed: result[0] === 1,
remaining: result[1],
resetMs,
};
}
middleware() {
return async (req: Request, res: Response, next: NextFunction) => {
const key = (req as any).user?.id
? `user:#123;(req as any).user.id}`
: `ip:#123;req.ip}`;
const { allowed, remaining, resetMs } = await this.checkLimit(key);
res.setHeader('X-RateLimit-Limit', this.config.capacity);
res.setHeader('X-RateLimit-Remaining', remaining);
res.setHeader('X-RateLimit-Reset', Date.now() + resetMs);
res.setHeader('Retry-After', Math.ceil(resetMs / 1000));
if (!allowed) {
return res.status(429).json({
error: 'Too Many Requests',
message: `Rate limit exceeded. Retry after #123;Math.ceil(resetMs / 1000)}s`,
retryAfter: Math.ceil(resetMs / 1000),
});
}
next();
};
}
}
// Differentiated limiters per endpoint type
export const apiLimiter = new TokenBucketLimiter({ capacity: 100, refillRate: 10 });
export const authLimiter = new TokenBucketLimiter({ capacity: 5, refillRate: 0.017 });
export const uploadLimiter = new TokenBucketLimiter({ capacity: 10, refillRate: 0.1 });
Sliding Window Algorithm
Fixed window has a known weakness: an attacker can concentrate N requests at the end of one window
and N requests at the start of the next, obtaining 2N requests in quick succession without violating
the technical limit. Sliding window solves this by maintaining an exact count per time window that
"slides" with time, using a Redis Sorted Set where each element has the request timestamp as its
score.
// Sliding Window Counter with Redis Sorted Set
class SlidingWindowLimiter {
constructor(
private limit: number, // Max requests in the window
private windowMs: number // Window size in milliseconds
) {}
async isAllowed(identifier: string): Promise<{
allowed: boolean;
count: number;
resetMs: number;
}> {
const now = Date.now();
const windowStart = now - this.windowMs;
const key = `rl:sw:#123;identifier}`;
const pipeline = redis.pipeline();
// Remove entries outside the current window
pipeline.zremrangebyscore(key, '-inf', windowStart);
// Add current request with unique member
pipeline.zadd(key, now, `#123;now}:#123;Math.random().toString(36).slice(2)}`);
// Count requests in current window
pipeline.zcard(key);
// Auto-expire to prevent orphaned keys
pipeline.pexpire(key, this.windowMs);
const results = await pipeline.exec();
const count = results![2][1] as number;
const oldest = await redis.zrange(key, 0, 0, 'WITHSCORES');
const resetMs = oldest.length > 1
? Math.max(0, parseInt(oldest[1]) + this.windowMs - now)
: this.windowMs;
return { allowed: count <= this.limit, count, resetMs };
}
}
// Express middleware factory with customizable key generator
function slidingWindowMiddleware(
limit: number,
windowMs: number,
keyGen?: (req: Request) => string
) {
const limiter = new SlidingWindowLimiter(limit, windowMs);
return async (req: Request, res: Response, next: NextFunction) => {
const key = keyGen
? keyGen(req)
: ((req as any).user?.id ?? req.ip ?? 'anon');
const { allowed, count, resetMs } = await limiter.isAllowed(key);
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - count));
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + resetMs).toISOString());
if (!allowed) {
return res.status(429).json({
error: 'Too Many Requests',
retryAfter: Math.ceil(resetMs / 1000),
});
}
next();
};
}
// Usage: global + endpoint-specific limits
app.use('/api/', slidingWindowMiddleware(1000, 60_000));
app.use('/api/auth/', slidingWindowMiddleware(10, 15 * 60_000));
app.post('/api/payments', slidingWindowMiddleware(
5, 60 * 60_000,
(req) => `pay:#123;(req as any).user!.id}` // Per-user instead of per-IP
));
Token Bucket vs Sliding Window: Which to Choose
Criterion
Token Bucket
Sliding Window
Traffic bursts
Allows bursts up to bucket capacity
Applies uniform limit, no burst
Limit precision
Approximate (depends on refill rate)
Precise to the millisecond
Redis storage
2 values per key (lightweight hash)
N members per key (sorted set, proportional to traffic)
Behavior at peaks
Absorbs natural spikes without errors
Strict: returns 429 immediately past the limit
Ideal use case
Public APIs, CDN, variable user traffic
Critical endpoints: auth, payments, OTP
OAuth 2.1 and JWT: Modern API Authentication
OAuth 2.1 (RFC draft) consolidates OAuth 2.0 security best practices. The key differences from
OAuth 2.0 are: PKCE mandatory for all clients (not just public clients),
implicit flow removed, resource owner password credentials flow
removed, and exact redirect URI matching required (no wildcard pattern
matching). If your OAuth 2.0 implementation already followed the OWASP Security Cheat Sheet,
the migration to OAuth 2.1 requires minimal changes.
Granular Scopes: Least Privilege for APIs
OAuth scopes define what a token can do. Overly broad scopes like read:all or
admin violate the principle of least privilege. A stolen token with scope
invoices:read:own causes far less damage than one with read:all.
Scope granularity should reflect the actual operations of your API.
// Granular OAuth scopes for a billing API
const SCOPES = {
// Format: resource:operation:context
'invoices:read:own': 'Read own invoices',
'invoices:read:all': 'Read all invoices (admin only)',
'invoices:create': 'Create new invoices',
'invoices:update:own': 'Update own invoices',
'invoices:delete:own': 'Delete own invoices',
'customers:read:own': 'Read own customer profile',
'customers:read:all': 'Read all customers (admin only)',
'reports:read:financial': 'Access financial reports',
'payments:create': 'Process payments',
'webhooks:manage': 'Manage webhooks',
} as const;
type Scope = keyof typeof SCOPES;
// Scope checking middleware
function requireScope(...requiredScopes: Scope[]) {
return (req: Request, res: Response, next: NextFunction) => {
const tokenScopes: string[] = ((req as any).user?.scope ?? '').split(' ');
const hasScope = requiredScopes.every(s => tokenScopes.includes(s));
if (!hasScope) {
return res.status(403).json({
error: 'insufficient_scope',
required: requiredScopes,
granted: tokenScopes,
});
}
next();
};
}
// Routes with granular scope checking
app.get('/api/invoices',
authenticate,
requireScope('invoices:read:own'),
async (req, res) => {
const invoices = await Invoice.find({ userId: (req as any).user.id });
res.json(invoices);
}
);
// Separate admin scope — not just a role check
app.get('/api/admin/invoices',
authenticate,
requireScope('invoices:read:all'),
async (req, res) => {
const invoices = await Invoice.find({});
res.json(invoices);
}
);
JWT: The 6 Fatal Mistakes
JWT is widely used but equally widely misimplemented. These six patterns turn a solid
authentication mechanism into a critical vulnerability. Each corresponds to a documented CVE
or a well-known real-world attack pattern.
// MISTAKE 1: Accepting the 'none' algorithm
// CVE-2015-9235 — many pre-2016 JWT libraries accepted this
// An attacker modifies header.alg = "none" and removes the signature
// VULNERABLE:
const decoded = jwt.verify(token, secret); // Accepts any algorithm by default
// CORRECT: Explicit algorithm whitelist
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // RS256 for distributed systems, or HS256 with strong key
});
// MISTAKE 2: Weak symmetric key
// VULNERABLE:
const token = jwt.sign(payload, 'secret'); // Brute-forceable in seconds
// CORRECT: Use RS256 (asymmetric) or HS256 with strong key (256+ bits)
// Generate RS256 keys:
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem
// MISTAKE 3: No claims validation
// VULNERABLE: Decoding without verifying iss, aud, exp
const payload = jwt.decode(token); // No signature verification!
// CORRECT: Full claims validation
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
// exp is checked automatically by jsonwebtoken
});
// MISTAKE 4: Access token with excessive lifetime
// VULNERABLE: 30-day window for attackers to exploit a stolen token
const token = jwt.sign(payload, secret, { expiresIn: '30d' });
// CORRECT: Short-lived access token + opaque refresh token
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
scope: user.scopes.join(' '),
jti: crypto.randomUUID(), // Unique JWT ID for blacklisting
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
}
);
// Refresh token: opaque, stored in DB, revocable
const refreshToken = crypto.randomBytes(32).toString('base64url');
await RefreshToken.create({
token: await bcrypt.hash(refreshToken, 12), // Hashed in DB, never plain text
userId: user.id,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
userAgent: req.get('User-Agent') ?? '',
ipAddress: req.ip ?? '',
});
// MISTAKE 5: Refresh token without rotation
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
const stored = await RefreshToken.findOne({
userId: (req as any).session?.userId,
used: false,
expiresAt: { $gt: new Date() },
});
if (!stored || !(await bcrypt.compare(refreshToken, stored.token))) {
// Token reused or invalid: REVOKE ALL tokens (possible theft indicator)
await RefreshToken.deleteMany({ userId: stored?.userId });
return res.status(401).json({ error: 'Invalid refresh token' });
}
await stored.updateOne({ used: true }); // Invalidate old token
const newAccessToken = generateAccessToken(stored.userId);
const newRefreshToken = crypto.randomBytes(32).toString('base64url');
await RefreshToken.create({
token: await bcrypt.hash(newRefreshToken, 12),
userId: stored.userId,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});
res.cookie('rt', newRefreshToken, {
httpOnly: true, secure: true, sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000,
path: '/api/auth/refresh',
});
res.json({ accessToken: newAccessToken });
});
// MISTAKE 6: JWT in localStorage (XSS vulnerability)
// CORRECT: access token in-memory, refresh token in HttpOnly cookie
// See Angular checklist section for the client-side implementation
API Keys: Secure Management for Server-to-Server Integrations
API keys are the right choice for machine-to-machine communication where the client is controlled
by the account owner (automation scripts, third-party backends, CI/CD integrations). They are
simpler than OAuth but require careful management: an API key must be treated like a password —
never exposed in logs or source code.
// Secure API key management — inspired by Stripe's approach
import crypto from 'crypto';
import { timingSafeEqual } from 'crypto';
// Identifiable prefix format: sk_live_xxx = production, sk_test_xxx = sandbox
function generateApiKey(env: 'live' | 'test' = 'live'): {
key: string;
hash: string;
prefix: string;
} {
const random = crypto.randomBytes(24).toString('base64url');
const key = `sk_#123;env}_#123;random}`;
const prefix = key.slice(0, 10) + '...'; // Safe display in UI
// Store SHA-256 hash: if DB is compromised, keys remain secure
const hash = crypto.createHash('sha256').update(key).digest('hex');
return { key, hash, prefix };
}
// API key authentication middleware
async function authenticateApiKey(req: Request, res: Response, next: NextFunction) {
const rawKey =
req.headers['x-api-key'] as string ??
req.headers.authorization?.replace('Bearer ', '');
if (!rawKey) {
return res.status(401).json({ error: 'API key required' });
}
const incomingHash = crypto.createHash('sha256').update(rawKey).digest('hex');
const storedKey = await ApiKey.findOne({
hash: incomingHash,
active: true,
expiresAt: { $gt: new Date() },
}).populate('owner');
if (!storedKey) {
// Timing-safe comparison: prevents timing attacks to enumerate valid keys
timingSafeEqual(
Buffer.from(incomingHash, 'hex'),
Buffer.alloc(32, 0)
);
return res.status(401).json({ error: 'Invalid or expired API key' });
}
// Update audit metadata
await ApiKey.findByIdAndUpdate(storedKey._id, {
lastUsedAt: new Date(),
$inc: { requestCount: 1 },
lastUsedIp: req.ip,
});
(req as any).user = storedKey.owner;
next();
}
// Key creation endpoint with scopes and custom rate limit
app.post('/api/keys', authenticate, async (req, res) => {
const schema = z.object({
name: z.string().min(1).max(100),
scopes: z.array(z.string()).min(1),
expiresInDays: z.number().int().min(1).max(365).default(90),
environment: z.enum(['live', 'test']).default('live'),
});
const parsed = schema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
const { key, hash, prefix } = generateApiKey(parsed.data.environment);
await ApiKey.create({
name: parsed.data.name,
hash, // Only hash stored in DB — never plain text
prefix, // For UI display without exposing the key
ownerId: (req as any).user.id,
scopes: parsed.data.scopes,
expiresAt: new Date(Date.now() + parsed.data.expiresInDays * 86_400_000),
});
// Return key ONCE only — Stripe/GitHub pattern
res.status(201).json({
key, // Only shown here, not retrievable again
prefix,
message: 'Store this key securely. It will not be shown again.',
});
});
Input Validation with Zod
Input validation is the first line of defense against injection, mass assignment, and unexpected
behavior. For TypeScript APIs, Zod is the tool of choice in 2025: define the
schema once and get both runtime validation and automatically inferred TypeScript types, with no
duplication between interfaces and runtime validators. Every endpoint must validate body, path
params, and query string separately.
// Comprehensive input validation with Zod
import { z } from 'zod';
const CreateOrderSchema = z.object({
// UUID format: prevents injection via malformed IDs
customerId: z.string().uuid('Invalid customer ID'),
// Explicit length limits: prevents DoS via oversized strings
notes: z.string().max(1000).optional().transform(v => v?.trim()),
// Array with bounds: prevents mass insertion
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100),
unitPrice: z.number().positive().max(99_999.99),
})).min(1).max(50),
// Enum: only predefined values, no arbitrary strings
shippingMethod: z.enum(['standard', 'express', 'overnight']),
// Date with semantic validation
requestedDelivery: z.string().datetime().transform(v => new Date(v))
.refine(d => d > new Date(), 'Delivery date must be in the future')
.optional(),
// URL with HTTPS requirement
webhookUrl: z.string().url()
.refine(u => u.startsWith('https://'), 'Webhook URL must use HTTPS')
.optional(),
});
type CreateOrderInput = z.infer<typeof CreateOrderSchema>;
// Reusable validation middleware
function validateBody<T extends z.ZodType>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
fieldErrors: result.error.flatten().fieldErrors,
});
}
req.body = result.data; // Replace with validated/transformed data
next();
};
}
// Safe pagination schema
const PaginationSchema = z.object({
page: z.coerce.number().int().min(1).max(10_000).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
order: z.enum(['asc', 'desc']).default('desc'),
q: z.string().max(100).optional().transform(v => v?.replace(/[^\w\s-]/g, '')),
});
// Usage in route
app.post(
'/api/orders',
authenticate,
requireScope('orders:create'),
apiLimiter.middleware(),
validateBody(CreateOrderSchema),
async (req: Request, res: Response) => {
const body = req.body as CreateOrderInput;
const order = await OrderService.create(body, (req as any).user.id);
res.status(201).json(order);
}
);
Secure CORS Configuration
CORS is often the first configuration a developer touches when API calls fail in development.
The most common wrong answer: Access-Control-Allow-Origin: *. This disables
same-origin protection for your entire API. Even worse,
Access-Control-Allow-Origin: * combined with credentials: true
is rejected by all modern browsers by design — it should be an immediate red flag in code review.
An API gateway centralizes cross-cutting concerns like authentication, rate limiting, logging,
and request transformation. In a microservices architecture, it prevents duplicating security
logic in every service. The Express middleware composition must follow a specific order: security
headers before any processing, then rate limiting, then authentication, then validation, then routes.
// API Gateway middleware stack — ordering is critical for security
import express from 'express';
import helmet from 'helmet';
import { v4 as uuidv4 } from 'uuid';
const app = express();
// STEP 1: Correlation ID — trace every request end-to-end
app.use((req: Request, res: Response, next: NextFunction) => {
const cid = (req.headers['x-correlation-id'] as string) ?? uuidv4();
(req as any).correlationId = cid;
res.setHeader('X-Correlation-ID', cid);
next();
});
// STEP 2: Security headers first
app.use(helmet());
app.use(cors(corsOptions));
// STEP 3: Body parsing with size limits (DoS prevention)
app.use(express.json({ limit: '10kb', strict: true }));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
// STEP 4: Global rate limiting before authentication
app.use('/api/', slidingWindowMiddleware(5000, 60_000));
// STEP 5: Authentication
app.use('/api/v1/', authenticateRequest);
// STEP 6: Per-user rate limiting (after auth, so we have user ID)
app.use('/api/v1/auth/', slidingWindowMiddleware(10, 15 * 60_000,
(req) => `auth:#123;req.ip}`
));
// STEP 7: Application routes
app.use('/api/v1/', v1Router);
// STEP 8: Secure error handler — always last
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const correlationId = (req as any).correlationId;
// Detailed internal logging
console.error({
correlationId,
error: err.message,
stack: err.stack,
url: req.url,
userId: (req as any).user?.id ?? 'anonymous',
});
// Safe external response: no internals in production
if (process.env.NODE_ENV === 'production') {
res.status(500).json({
error: 'Internal Server Error',
correlationId, // Enables support to find the corresponding log
});
} else {
res.status(500).json({ error: err.message, correlationId });
}
});
// Circuit breaker for upstream services
import CircuitBreaker from 'opossum';
const paymentBreaker = new CircuitBreaker(
async (payload: unknown) => {
const resp = await fetch('https://payment-svc/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3000),
});
if (!resp.ok) throw new Error(`Payment error: #123;resp.status}`);
return resp.json();
},
{
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30_000,
volumeThreshold: 5,
}
);
paymentBreaker.fallback(() => {
throw new Error('Payment service temporarily unavailable');
});
Monitoring: Detecting Attacks in Real Time
An effective API monitoring system focuses on security-specific metrics: 401/403 error rates
per IP and per endpoint, ID enumeration patterns on resources, requests anomalous in payload
size, and access to undocumented or deprecated endpoints. Generic application metrics are not
enough — you need security-oriented counters.
The Angular frontend has specific responsibilities in API security. Where you store tokens directly
determines the XSS attack surface. A centralized HTTP interceptor avoids distributing critical
security logic across dozens of services.
Only hash in DB, never plain text; timing-safe comparison; prefix for identification
MEDIUM
Anti-Patterns to Avoid Completely
JWT in localStorage: Accessible to any JS script on the page. Use in-memory for the access token and an HttpOnly cookie for the refresh token
CORS wildcard with credentials:Access-Control-Allow-Origin: * with credentials: true is rejected by all modern browsers — it is a misconfiguration, not a trade-off
Rate limiting on IP only: A distributed botnet bypasses per-IP limits trivially. Combine IP-based and user-based rate limiting
Overly broad scopes:admin or read:all violate least privilege. A stolen token with invoices:read:own causes far less damage
Stack trace in production: Reveals internal paths, library versions, and DB structure. Always use an error handler that obscures details in production
Unspecified JWT algorithm: Always specify algorithms: ['RS256']. Some JWT libraries with default configuration accept the none algorithm
BOLA not verified: 40% of API attacks exploit BOLA. Every endpoint that accepts an ID must verify the user owns that resource
Conclusions
API security is not solved with a single tool or configuration. It is a continuous practice that
requires attention at every layer: from endpoint design (systematic BOLA verification, granular
scopes), to rate limiting implementation (choosing the right algorithm for each use case), to
correct OAuth 2.1 and JWT management (RS256, short expiry, refresh token rotation), through to
monitoring for detecting attack patterns before they cause significant damage.
Data from 2024-2025 shows that most API incidents do not involve exotic vulnerabilities: they
involve basic authorization errors (not verifying that a resource belongs to the requesting user),
permissive CORS configurations, JWT tokens without expiry, and lack of rate limiting on
authentication endpoints. These are solvable problems with well-known patterns applied with
discipline.
If you use AI-assisted coding tools, remember that 45% of AI-generated code fails
security tests: generated code often implements authentication but omits ownership
checks on resources (BOLA), uses superficial IP-only rate limiting, and accepts arbitrary
input fields without validation (mass assignment). Use the checklist in this article as a
systematic verification point after every AI coding session.