← ← Back to all posts

Advanced API Security: OAuth2, GraphQL, and DDoS Protection

2025-11-06 · Benja

Technical article on advanced API security: OAuth2 with PKCE, hardened GraphQL, DDoS mitigation, WAF, and monitoring.

Advanced API Security: OAuth2, GraphQL, and DDoS Protection

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_verifier and code_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_token on 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.