Advanced API Security: OAuth2, GraphQL and DDoS Protection
As APIs become the core of products and platforms, their attack surface grows. A simple token is no longer enough: you now need properly implemented OAuth2, locked-down GraphQL, DDoS mitigation, a basic WAF, and real-time monitoring.
In this article we build a defense-in-depth architecture with Node.js examples you can adapt to your projects. We also review and refine the code snippets to make them more consistent and robust.
1. Advanced OAuth2 Security
OAuth2 is the standard for delegating secure access to resources, but incomplete implementations often open the door to attacks. Here we combine Authorization Code + PKCE, strict client validation, token expiration, and a client credentials flow for machine-to-machine scenarios.
1.1 Full OAuth2 Implementation with PKCE
The following simplified OAuth2 server includes:
- Generation of
code_verifierandcode_challenge(PKCE). - Authorization endpoint with client, redirect URI and PKCE validation.
- Token endpoint with code, PKCE, redirect URI and expiration checks.
- Middleware to validate the
access_tokenon protected routes.
const express = require('express');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const app = express();
class OAuth2Server {
constructor() {
this.authorizationCodes = new Map();
this.accessTokens = new Map();
this.refreshTokens = new Map();
this.clients = new Map();
}
// Generate code_verifier for PKCE
generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
// Generate code_challenge (S256)
generateCodeChallenge(codeVerifier) {
return crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
}
// Authorization endpoint
authorizationEndpoint(req, res) {
const {
client_id,
redirect_uri,
response_type,
scope,
state,
code_challenge,
code_challenge_method
} = req.query;
if (response_type !== 'code') {
return res.status(400).json({
error: 'unsupported_response_type',
error_description: 'Only response_type=code is supported'
});
}
// Validate client
const client = this.clients.get(client_id);
if (!client) {
return res.status(400).json({
error: 'invalid_client',
error_description: 'Client not found'
});
}
// Validate redirect URI
if (!client.redirect_uris.includes(redirect_uri)) {
return res.status(400).json({
error: 'invalid_redirect_uri',
error_description: 'Invalid redirect URI'
});
}
// Validate PKCE (optional but recommended)
if (code_challenge && !['plain', 'S256'].includes(code_challenge_method)) {
return res.status(400).json({
error: 'invalid_request',
error_description: 'Unsupported challenge method'
});
}
// Simulate login page
if (!req.session || !req.session.user) {
const query = new URLSearchParams(req.query).toString();
return res.redirect(`/login?${query}`);
}
const user = req.session.user;
const authCode = uuidv4();
// Store authorization code
this.authorizationCodes.set(authCode, {
client_id,
redirect_uri,
user_id: user.id,
scope: scope ? scope.split(' ') : [],
code_challenge,
code_challenge_method,
expires_at: Date.now() + 10 * 60 * 1000 // 10 minutes
});
// Redirect back with code
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.set('code', authCode);
if (state) redirectUrl.searchParams.set('state', state);
return res.redirect(redirectUrl.toString());
}
// Token endpoint
async tokenEndpoint(req, res) {
const {
grant_type,
code,
redirect_uri,
client_id,
client_secret,
code_verifier
} = req.body;
try {
// Validate grant type
if (grant_type !== 'authorization_code') {
return res.status(400).json({
error: 'unsupported_grant_type',
error_description: 'Unsupported grant type'
});
}
// Validate client credentials
const client = this.validateClient(client_id, client_secret);
if (!client) {
return res.status(401).json({
error: 'invalid_client',
error_description: 'Invalid client credentials'
});
}
// Look up authorization code
const authCodeData = this.authorizationCodes.get(code);
if (!authCodeData) {
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Invalid authorization code'
});
}
// Check expiration
if (Date.now() > authCodeData.expires_at) {
this.authorizationCodes.delete(code);
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Authorization code has expired'
});
}
// Validate redirect URI
if (authCodeData.redirect_uri !== redirect_uri) {
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Redirect URI does not match'
});
}
// Validate PKCE if it was used during authorization
if (authCodeData.code_challenge) {
if (!code_verifier) {
return res.status(400).json({
error: 'invalid_request',
error_description: 'Code verifier is required'
});
}
const expectedChallenge =
authCodeData.code_challenge_method === 'S256'
? this.generateCodeChallenge(code_verifier)
: code_verifier;
if (authCodeData.code_challenge !== expectedChallenge) {
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Invalid code verifier'
});
}
}
// Generate tokens
const accessToken = this.generateToken();
const refreshToken = this.generateToken();
// Store access token
this.accessTokens.set(accessToken, {
client_id: authCodeData.client_id,
user_id: authCodeData.user_id,
scope: authCodeData.scope,
expires_at: Date.now() + 60 * 60 * 1000 // 1 hour
});
// Store refresh token
this.refreshTokens.set(refreshToken, {
client_id: authCodeData.client_id,
user_id: authCodeData.user_id,
scope: authCodeData.scope
});
// One-time use: remove authorization code
this.authorizationCodes.delete(code);
return res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: refreshToken,
scope: authCodeData.scope.join(' ')
});
} catch (error) {
console.error('Error in token endpoint:', error);
return res.status(500).json({
error: 'server_error',
error_description: 'Internal server error'
});
}
}
// Access token validation middleware
validateToken(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Access token required'
});
}
const token = authHeader.substring(7);
const tokenData = this.accessTokens.get(token);
if (!tokenData) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Invalid access token'
});
}
if (Date.now() > tokenData.expires_at) {
this.accessTokens.delete(token);
return res.status(401).json({
error: 'invalid_token',
error_description: 'Access token has expired'
});
}
req.oauth = {
client_id: tokenData.client_id,
user_id: tokenData.user_id,
scope: tokenData.scope
};
return next();
}
validateClient(clientId, clientSecret) {
const client = this.clients.get(clientId);
return client && client.client_secret === clientSecret ? client : null;
}
generateToken() {
return crypto.randomBytes(32).toString('hex');
}
}
// OAuth2 server usage
const oauthServer = new OAuth2Server();
// Register example client
oauthServer.clients.set('my-client-id', {
client_id: 'my-client-id',
client_secret: 'my-client-secret',
redirect_uris: ['https://myapp.com/callback'],
scope: ['read', 'write']
});
// Global middleware
app.use(express.json());
// OAuth2 routes
app.get('/oauth/authorize', (req, res) => {
oauthServer.authorizationEndpoint(req, res);
});
app.post('/oauth/token', (req, res) => {
oauthServer.tokenEndpoint(req, res);
});
// Protected route with OAuth2
app.get(
'/api/protected',
(req, res, next) => oauthServer.validateToken(req, res, next),
(req, res) => {
res.json({
message: 'Access granted to protected resource',
user_id: req.oauth.user_id,
scope: req.oauth.scope
});
}
);
1.2 Client Credentials Flow for Machine-to-Machine
The client credentials flow allows two services to authenticate to each other with no human user. It is key for microservices and backend jobs.
class ClientCredentialsFlow {
async clientCredentialsToken(req, res) {
const { grant_type, client_id, client_secret, scope } = req.body;
if (grant_type !== 'client_credentials') {
return res.status(400).json({
error: 'unsupported_grant_type'
});
}
// Validate client
const client = oauthServer.validateClient(client_id, client_secret);
if (!client) {
return res.status(401).json({
error: 'invalid_client'
});
}
// Generate access token
const accessToken = oauthServer.generateToken();
const requestedScope = scope ? scope.split(' ') : client.scope;
// Verify requested scopes
const validScope = requestedScope.every(s => client.scope.includes(s));
if (!validScope) {
return res.status(400).json({
error: 'invalid_scope'
});
}
oauthServer.accessTokens.set(accessToken, {
client_id: client.client_id,
user_id: null, // No user in this flow
scope: requestedScope,
expires_at: Date.now() + 60 * 60 * 1000
});
return res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
scope: requestedScope.join(' ')
});
}
}
// Example route for client credentials
const clientCredentialsFlow = new ClientCredentialsFlow();
app.post('/oauth/client-token', (req, res) => {
clientCredentialsFlow.clientCredentialsToken(req, res);
});
2. Advanced GraphQL Security
GraphQL is powerful but easy to abuse: deep queries, aggressive introspection, complexity-based attacks, and more. Here we combine depth limits, complexity limits, field-level permissions, and additional validations.
2.1 Hardened GraphQL Server
const { ApolloServer, gql } = require('apollo-server-express');
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
// rateLimitDirective could be wired with an external limiter
// const { rateLimitDirective } = require('graphql-rate-limit-directive');
class SecureGraphQLServer {
constructor() {
this.typeDefs = gql`
directive @rateLimit(
max: Int
window: String
message: String
) on FIELD_DEFINITION
type Query {
users(limit: Int = 10): [User] @rateLimit(max: 10, window: "1m")
user(id: ID!): User
posts(limit: Int = 10): [Post]
}
type Mutation {
createPost(input: PostInput!): Post @rateLimit(max: 5, window: "5m")
deletePost(id: ID!): Boolean
}
type User {
id: ID!
email: String!
posts: [Post]
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
input PostInput {
title: String!
content: String!
}
`;
this.resolvers = {
Query: {
users: async (_, { limit }, context) => {
await this.authorize(context.user, ['users:read']);
return this.getUsers(limit);
},
user: async (_, { id }, context) => {
await this.authorize(context.user, ['users:read']);
return this.getUser(id);
},
posts: async (_, { limit }, context) => {
await this.authorize(context.user, ['posts:read']);
return this.getPosts(limit);
}
},
Mutation: {
createPost: async (_, { input }, context) => {
await this.authorize(context.user, ['posts:write']);
return this.createPost(input, context.user);
},
deletePost: async (_, { id }, context) => {
await this.authorize(context.user, ['posts:delete']);
return this.deletePost(id, context.user);
}
},
User: {
posts: (user, _, context) => {
// Only allow viewing own posts unless admin
if (
context.user.id !== user.id &&
!context.user.roles.includes('admin')
) {
return [];
}
return this.getUserPosts(user.id);
}
}
};
}
createServer() {
const validationRules = [
depthLimit(10), // Max depth
createComplexityLimitRule(1000, {
onCost: cost => {
console.log('Query complexity:', cost);
},
formatErrorMessage: cost =>
`Query too complex: ${cost}. Maximum allowed: 1000`
})
];
return new ApolloServer({
typeDefs: this.typeDefs,
resolvers: this.resolvers,
validationRules,
context: ({ req }) => this.createContext(req),
plugins: [this.securityPlugin()],
formatError: err => this.formatError(err)
});
}
async createContext(req) {
// JWT authentication (real implementation omitted)
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.replace('Bearer ', '')
: null;
let user = null;
if (token) {
try {
user = await this.verifyJWT(token);
} catch (error) {
console.warn('Invalid JWT token:', error.message);
}
}
return {
user,
ip: req.ip,
userAgent: req.get('User-Agent')
};
}
securityPlugin() {
return {
requestDidStart: () => ({
didResolveOperation: async requestContext => {
await this.validateQuery(requestContext);
},
didEncounterErrors: async requestContext => {
this.logSecurityEvents(requestContext);
}
})
};
}
async validateQuery(requestContext) {
const { request, context } = requestContext;
const query = request.query || '';
this.preventRecursiveQueries(query);
this.validateFieldLimits(query);
if (this.isSuspiciousQuery(query)) {
await this.logSuspiciousActivity(context, query);
}
}
preventRecursiveQueries(query) {
const recursivePatterns = [
/user\s*{\s*posts\s*{\s*author\s*{\s*posts/gi,
/posts\s*{\s*author\s*{\s*posts\s*{\s*author/gi
];
for (const pattern of recursivePatterns) {
if (pattern.test(query)) {
throw new Error('Recursive query detected and blocked');
}
}
}
validateFieldLimits(query) {
const fieldCount = (query.match(/\w+\s*{/g) || []).length;
if (fieldCount > 50) {
throw new Error(
'Too many fields requested in the query. Limit: 50 fields'
);
}
}
isSuspiciousQuery(query) {
const suspiciousPatterns = [
/__schema|__type|__typename/gi, // Abusive introspection
/(\w+)\s*{\s*\1/gi, // Self-reference
/\.\.\./g // Excessive fragments
];
return suspiciousPatterns.some(pattern => pattern.test(query));
}
async authorize(user, requiredPermissions) {
if (!user) {
throw new Error('Authentication required');
}
const hasPermission = requiredPermissions.every(permission =>
user.permissions.includes(permission)
);
if (!hasPermission) {
throw new Error('Insufficient permissions');
}
}
formatError(err) {
const { message, locations, path } = err;
if (message.includes('Database') || message.includes('Internal')) {
console.error('Internal error:', err);
return {
message: 'Internal server error',
code: 'INTERNAL_ERROR'
};
}
return {
message,
locations,
path,
code: err.extensions?.code || 'GRAPHQL_ERROR'
};
}
// Domain methods (stubs)
async verifyJWT(token) {
// Replace with real JWT verification
return {
id: 'user-123',
roles: ['user'],
permissions: ['users:read', 'posts:read']
};
}
async getUsers(limit) {
return [];
}
async getUser(id) {
return null;
}
async getPosts(limit) {
return [];
}
async createPost(input, user) {
return { id: 'post-1', title: input.title, content: input.content, author: user };
}
async deletePost(id, user) {
return true;
}
async getUserPosts(userId) {
return [];
}
logSecurityEvents(requestContext) {
console.warn('GraphQL errors:', requestContext.errors);
}
async logSuspiciousActivity(context, query) {
console.warn('Suspicious query detected:', {
ip: context.ip,
user: context.user?.id,
query
});
}
}
// Secure GraphQL server bootstrap
async function startSecureGraphQL(app) {
const secureGraphQL = new SecureGraphQLServer();
const server = secureGraphQL.createServer();
await server.start();
server.applyMiddleware({ app, path: '/graphql' });
}
2.2 Advanced Rate Limiting for GraphQL
Beyond complexity, it is useful to limit the frequency of calls to specific fields. We use Redis to count requests per user/IP, operation and field.
class GraphQLRateLimiter {
constructor(redisClient) {
this.redis = redisClient;
this.limits = {
Query: {
users: { window: '1m', max: 10 },
posts: { window: '1m', max: 20 }
},
Mutation: {
createPost: { window: '5m', max: 5 },
deletePost: { window: '1h', max: 10 }
}
};
}
async checkRateLimit(context, fieldName, operation) {
const limitConfig = this.limits[operation]?.[fieldName];
if (!limitConfig) return;
const key = `graphql_rate_limit:${context.user?.id || context.ip}:${operation}:${fieldName}`;
const now = Date.now();
const windowMs = this.parseWindow(limitConfig.window);
const pipeline = this.redis.pipeline();
pipeline.zremrangebyscore(key, 0, now - windowMs);
pipeline.zadd(key, now, now);
pipeline.zcard(key);
pipeline.expire(key, Math.ceil(windowMs / 1000));
const results = await pipeline.exec();
const requestCount = results[2][1];
if (requestCount > limitConfig.max) {
throw new Error(
`Rate limit exceeded for ${fieldName}. ` +
`Maximum ${limitConfig.max} requests per ${limitConfig.window}`
);
}
return {
limit: limitConfig.max,
remaining: Math.max(0, limitConfig.max - requestCount),
reset: now + windowMs
};
}
parseWindow(window) {
const units = {
s: 1000,
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000
};
const match = window.match(/^(\d+)([smhd])$/);
if (!match) return 60000; // Default 1 minute
const [, amount, unit] = match;
return parseInt(amount, 10) * units[unit];
}
}
// Simple usage inside resolvers
const graphQLLimiter = new GraphQLRateLimiter(redisClient);
const resolvers = {
Query: {
users: async (_, { limit }, context, info) => {
await graphQLLimiter.checkRateLimit(
context,
info.fieldName,
info.parentType.name
);
// Real resolver implementation
return [];
}
}
};
3. Advanced DDoS Protection
No API is immune to denial-of-service attacks. You can mitigate a large part of them with:
- Rate limiting by IP, user, and endpoint.
- Burst detection and abnormal traffic patterns.
- Temporary blocking and challenges for suspicious traffic.
3.1 Layered DDoS Mitigation System
const crypto = require('crypto');
class DDoSProtectionSystem {
constructor(redisClient) {
this.redis = redisClient;
this.thresholds = {
IP: { requests: 100, window: 60000 }, // 100 req/min per IP
USER: { requests: 1000, window: 60000 }, // 1000 req/min per user
ENDPOINT: { requests: 500, window: 60000 } // 500 req/min per endpoint
};
}
async checkDDoSPotential(req) {
const ip = req.ip;
const userId = req.user?.id || null;
const endpoint = req.path;
const checks = [
this.checkIPRate(ip),
this.checkUserRate(userId),
this.checkEndpointRate(endpoint, ip),
this.checkBehaviorPatterns(req),
this.checkIPReputation(ip)
];
const results = await Promise.all(checks);
const isUnderAttack = results.some(result => result.isSuspicious);
if (isUnderAttack) {
await this.triggerMitigation(req, results);
return false;
}
return true;
}
async checkIPRate(ip) {
const key = `ddos:ip:${ip}`;
const now = Date.now();
const window = this.thresholds.IP.window;
const requestCount = await this.redis.zcount(key, now - window, now);
await this.redis.zadd(key, now, now);
await this.redis.expire(key, Math.ceil(window / 1000));
return {
type: 'IP_RATE',
isSuspicious: requestCount > this.thresholds.IP.requests,
severity: Math.min(requestCount / this.thresholds.IP.requests, 10),
details: { ip, requestCount, threshold: this.thresholds.IP.requests }
};
}
async checkUserRate(userId) {
if (!userId) {
return {
type: 'USER_RATE',
isSuspicious: false,
severity: 0,
details: {}
};
}
const key = `ddos:user:${userId}`;
const now = Date.now();
const window = this.thresholds.USER.window;
const requestCount = await this.redis.zcount(key, now - window, now);
await this.redis.zadd(key, now, now);
await this.redis.expire(key, Math.ceil(window / 1000));
return {
type: 'USER_RATE',
isSuspicious: requestCount > this.thresholds.USER.requests,
severity: Math.min(requestCount / this.thresholds.USER.requests, 10),
details: { userId, requestCount, threshold: this.thresholds.USER.requests }
};
}
async checkEndpointRate(endpoint, ip) {
const key = `ddos:endpoint:${endpoint}:${ip}`;
const now = Date.now();
const window = this.thresholds.ENDPOINT.window;
const requestCount = await this.redis.zcount(key, now - window, now);
await this.redis.zadd(key, now, now);
await this.redis.expire(key, Math.ceil(window / 1000));
return {
type: 'ENDPOINT_RATE',
isSuspicious: requestCount > this.thresholds.ENDPOINT.requests,
severity: Math.min(requestCount / this.thresholds.ENDPOINT.requests, 10),
details: { endpoint, requestCount, threshold: this.thresholds.ENDPOINT.requests }
};
}
async checkBehaviorPatterns(req) {
const patternChecks = [
this.checkUserAgent(req.get('User-Agent')),
this.checkRequestFrequency(req),
this.checkGeolocationAnomaly(req.ip),
this.checkURLPatterns(req.path)
];
const results = await Promise.all(patternChecks);
const suspiciousCount = results.filter(r => r.isSuspicious).length;
return {
type: 'BEHAVIOR_PATTERN',
isSuspicious: suspiciousCount >= 2,
severity: suspiciousCount / 4,
details: { patterns: results }
};
}
checkUserAgent(userAgent) {
const suspiciousUAs = [
'bot', 'crawler', 'scraper', 'python', 'curl', 'wget',
'mass', 'scan', 'hack', 'attack'
];
const isSuspicious = suspiciousUAs.some(term =>
userAgent?.toLowerCase().includes(term)
);
return { pattern: 'USER_AGENT', isSuspicious, details: { userAgent } };
}
async checkRequestFrequency(req) {
const key = `ddos:burst:${req.ip}`;
const now = Date.now();
const burstWindow = 1000; // 1 second
const recentRequests = await this.redis.zcount(key, now - burstWindow, now);
await this.redis.zadd(key, now, now);
await this.redis.expire(key, 2);
return {
pattern: 'REQUEST_BURST',
isSuspicious: recentRequests > 10, // More than 10 req/second
details: { recentRequests }
};
}
async checkGeolocationAnomaly(ip) {
// Placeholder: integrate with a geo-IP service if needed
return { pattern: 'GEO_ANOMALY', isSuspicious: false, details: { ip } };
}
checkURLPatterns(path) {
const suspiciousPatterns = [
/\.\.\//, // Path traversal
/\/\.env/, // Config files
/\/phpmyadmin/, // Admin tools
/\/wp-admin/, // WordPress admin
/\/api\/[^/]+\/\.\./ // API path traversal
];
const isSuspicious = suspiciousPatterns.some(pattern => pattern.test(path));
return { pattern: 'URL_PATTERN', isSuspicious, details: { path } };
}
async checkIPReputation(ip) {
// Placeholder: integration with IP reputation service
return { type: 'IP_REPUTATION', isSuspicious: false, severity: 0, details: { ip } };
}
async triggerMitigation(req, alerts) {
const ip = req.ip;
const suspiciousAlerts = alerts.filter(a => a.isSuspicious);
const severity = Math.max(...suspiciousAlerts.map(a => a.severity || 0), 0);
console.warn(`🚨 Activating DDoS mitigation for IP: ${ip}`, {
severity,
alerts: suspiciousAlerts
});
if (severity < 3) {
await this.enforceStrictRateLimit(ip);
} else if (severity < 7) {
await this.requireChallenge(ip);
} else {
await this.temporaryBlock(ip);
}
await this.alertSecurityTeam(req, suspiciousAlerts);
}
async enforceStrictRateLimit(ip) {
const key = `ddos:strict_limit:${ip}`;
await this.redis.setex(key, 300, 'enforced'); // 5 minutes
}
async requireChallenge(ip) {
const key = `ddos:challenge:${ip}`;
const challenge = crypto.randomBytes(8).toString('hex');
await this.redis.setex(key, 600, challenge); // 10 minutes
}
async temporaryBlock(ip) {
const key = `ddos:blocked:${ip}`;
await this.redis.setex(key, 1800, 'blocked'); // 30 minutes
await this.redis.sadd('ddos:blacklist', ip);
await this.redis.expire('ddos:blacklist', 1800);
}
async isIPBlocked(ip) {
const exists = await this.redis.exists(`ddos:blocked:${ip}`);
return Boolean(exists);
}
async alertSecurityTeam(req, alerts) {
// Hook for Slack/Email/SIEM integration
console.log('Notifying security team:', {
ip: req.ip,
alerts
});
}
}
// DDoS protection middleware
const ddosProtection = new DDoSProtectionSystem(redisClient);
app.use(async (req, res, next) => {
if (await ddosProtection.isIPBlocked(req.ip)) {
return res.status(429).json({
error: 'IP temporarily blocked due to suspicious activity',
code: 'IP_BLOCKED',
retryAfter: 1800
});
}
const allowed = await ddosProtection.checkDDoSPotential(req);
if (!allowed) {
return res.status(429).json({
error: 'Suspicious activity detected',
code: 'SUSPICIOUS_ACTIVITY'
});
}
return next();
});
3.2 Basic Web Application Firewall (WAF)
A lightweight WAF can catch obvious SQLi, XSS, path traversal and other attacks before they hit your actual application logic.
class SimpleWAF {
constructor() {
this.rules = [
{
name: 'SQL Injection',
pattern: /(\bUNION\b.*\bSELECT|\bDROP\b|\bINSERT\b|\bDELETE\b|\bUPDATE\b.*\bSET|\bOR\b.*=)/gi,
action: 'block'
},
{
name: 'XSS Attempt',
pattern: /(
4. Security Monitoring and Alerting
Blocking alone is not enough if you cannot see what is going on. A security dashboard lets you:
- Track attack attempts, blocks, and authentication failures.
- Identify the most problematic IPs.
- Trigger alerts when overall risk grows too high.
4.1 Real-Time Security Dashboard
class SecurityDashboard {
constructor(redisClient) {
this.redis = redisClient;
this.metrics = {
totalRequests: 0,
blockedRequests: 0,
authenticationFailures: 0,
rateLimitHits: 0,
ddosAlerts: 0
};
}
async recordSecurityEvent(event) {
const timestamp = Date.now();
const eventKey = `security:events:${timestamp}`;
await this.redis.hset(eventKey, {
type: event.type,
severity: event.severity,
ip: event.ip,
user: event.user || 'anonymous',
endpoint: event.endpoint,
details: JSON.stringify(event.details || {}),
timestamp: timestamp
});
await this.redis.expire(eventKey, 24 * 60 * 60); // 24 hours
this.updateMetrics(event);
if (event.severity >= 7) {
await this.sendAlert(event);
}
}
updateMetrics(event) {
this.metrics.totalRequests += 1;
switch (event.type) {
case 'AUTH_FAILURE':
this.metrics.authenticationFailures += 1;
break;
case 'RATE_LIMIT':
this.metrics.rateLimitHits += 1;
break;
case 'DDoS_ALERT':
this.metrics.ddosAlerts += 1;
break;
case 'REQUEST_BLOCKED':
this.metrics.blockedRequests += 1;
break;
default:
break;
}
}
async sendAlert(event) {
const alert = {
title: `Security Alert: ${event.type}`,
severity: event.severity,
ip: event.ip,
timestamp: new Date().toISOString(),
details: event.details
};
await this.sendToWebhook(alert);
await this.sendToLog(alert);
}
async sendToWebhook(alert) {
// Slack / Teams / Webhook integration
console.log('Sending alert to webhook:', alert.title);
}
async sendToLog(alert) {
// Centralized logging integration (ELK, Loki, etc.)
console.log('Recording alert in logs:', alert.title);
}
async getSecurityReport() {
const now = Date.now();
const oneHourAgo = now - 60 * 60 * 1000;
const eventKeys = await this.redis.keys('security:events:*');
const recentEvents = [];
for (const key of eventKeys) {
const timestamp = parseInt(key.split(':')[2], 10);
if (timestamp >= oneHourAgo) {
const event = await this.redis.hgetall(key);
recentEvents.push({
...event,
details: JSON.parse(event.details)
});
}
}
return {
metrics: this.metrics,
recentEvents: recentEvents.slice(-100),
topOffenders: await this.getTopOffenders(recentEvents),
riskAssessment: this.assessRisk(recentEvents)
};
}
async getTopOffenders(events) {
const ipCounts = {};
events.forEach(event => {
ipCounts[event.ip] = (ipCounts[event.ip] || 0) + 1;
});
return Object.entries(ipCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([ip, count]) => ({ ip, count }));
}
assessRisk(events) {
const highSeverityEvents = events.filter(e => e.severity >= 7);
const riskScore = Math.min(highSeverityEvents.length * 10, 100);
let level = 'LOW';
if (riskScore >= 70) level = 'HIGH';
else if (riskScore >= 30) level = 'MEDIUM';
return { score: riskScore, level };
}
}
// Protected security dashboard endpoint
app.get(
'/admin/security-dashboard',
authenticateToken,
authorize(['admin', 'security']),
async (req, res) => {
const dashboard = new SecurityDashboard(redisClient);
const report = await dashboard.getSecurityReport();
return res.json(report);
}
);
Conclusion
Modern API security is not solved by a single component. You need multiple layers: proper OAuth2, hardened GraphQL, smart rate limiting, DDoS protection, a WAF, and continuous monitoring.
The code above is not a drop-in production solution, but a solid starting point to build a defense-in-depth architecture and significantly raise the security level of your APIs.