Fehlerbehandlung

Fehlerbehandlung #

Häufige Validierungsfehler #

FehlerUrsacheLösung
Token expiredToken ist abgelaufenNeues Token anfordern
Invalid signatureFalscher Schlüssel oder manipuliertes TokenSchlüssel überprüfen
Hash mismatchPayload wurde verändertOriginale Payload verwenden
Missing claimsPflicht-Claims fehlenToken-Struktur korrigieren
Token not yet validnbf-Claim liegt in der ZukunftClock-Synchronisation prüfen
Replay attack detectedJTI bereits verwendetNeues Token generieren

Beispiel-Fehlerbehandlung #

app.post('/webhook', (req, res) => {
  try {
    const token = req.headers.authorization?.replace('Bearer ', '');
    const validatedToken = validateSWT(token, req.body, SECRET_KEY);
    
    // Webhook verarbeiten
    processWebhook(validatedToken.webhook, req.body);
    
    res.status(200).json({ status: 'success' });
  } catch (error) {
    console.error('Webhook-Validierung fehlgeschlagen:', error.message);
    
    // Spezifische Fehlerbehandlung
    if (error.message.includes('expired')) {
      return res.status(401).json({ 
        error: 'TOKEN_EXPIRED',
        message: 'Token ist abgelaufen'
      });
    }
    
    if (error.message.includes('signature')) {
      return res.status(401).json({ 
        error: 'INVALID_SIGNATURE',
        message: 'Token-Signatur ungültig'
      });
    }
    
    if (error.message.includes('Hash')) {
      return res.status(400).json({ 
        error: 'PAYLOAD_MISMATCH',
        message: 'Payload wurde verändert'
      });
    }
    
    // Allgemeiner Fehler
    res.status(401).json({ 
      error: 'UNAUTHORIZED', 
      message: 'Token-Validierung fehlgeschlagen'
    });
  }
});

Erweiterte Fehlerbehandlung #

Fehlerklassen definieren #

class SWTError extends Error {
  constructor(message, code, statusCode = 401) {
    super(message);
    this.name = 'SWTError';
    this.code = code;
    this.statusCode = statusCode;
  }
}

class TokenExpiredError extends SWTError {
  constructor() {
    super('Token ist abgelaufen', 'TOKEN_EXPIRED', 401);
  }
}

class InvalidSignatureError extends SWTError {
  constructor() {
    super('Token-Signatur ungültig', 'INVALID_SIGNATURE', 401);
  }
}

class PayloadMismatchError extends SWTError {
  constructor() {
    super('Payload-Hash stimmt nicht überein', 'PAYLOAD_MISMATCH', 400);
  }
}

class ReplayAttackError extends SWTError {
  constructor() {
    super('Token bereits verwendet', 'REPLAY_ATTACK', 401);
  }
}

Verbesserte Validierungsfunktion #

function validateSWT(token, payload, secret) {
  try {
    const decoded = jwt.verify(token, secret);
    
    // Pflicht-Claims prüfen
    const requiredClaims = ['webhook', 'iss', 'exp', 'nbf', 'iat', 'jti'];
    for (const claim of requiredClaims) {
      if (!decoded[claim]) {
        throw new SWTError(`Fehlender Pflicht-Claim: ${claim}`, 'MISSING_CLAIM', 400);
      }
    }
    
    // Zeitvalidierung
    const now = Math.floor(Date.now() / 1000);
    if (decoded.exp <= now) {
      throw new TokenExpiredError();
    }
    if (decoded.nbf > now) {
      throw new SWTError('Token noch nicht gültig', 'TOKEN_NOT_YET_VALID', 401);
    }
    
    // Payload-Hash validieren
    if (payload && decoded.webhook.hash) {
      const [algorithm, expectedHash] = decoded.webhook.hash.split(':');
      const actualHash = crypto.createHash(algorithm)
                              .update(JSON.stringify(payload))
                              .digest('hex');
      
      if (actualHash !== expectedHash) {
        throw new PayloadMismatchError();
      }
    }
    
    return decoded;
  } catch (error) {
    if (error instanceof SWTError) {
      throw error;
    }
    
    if (error.name === 'TokenExpiredError') {
      throw new TokenExpiredError();
    }
    
    if (error.name === 'JsonWebTokenError') {
      throw new InvalidSignatureError();
    }
    
    throw new SWTError('Unbekannter Validierungsfehler', 'VALIDATION_ERROR', 500);
  }
}

Logging und Monitoring #

Strukturiertes Logging #

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'webhook-errors.log', level: 'error' }),
    new winston.transports.File({ filename: 'webhook-combined.log' })
  ]
});

function logWebhookEvent(event, details, level = 'info') {
  logger.log({
    level: level,
    message: 'Webhook Event',
    event: event,
    details: details,
    timestamp: new Date().toISOString()
  });
}

// Verwendung
try {
  const validatedToken = validateSWT(token, req.body, SECRET_KEY);
  logWebhookEvent('webhook_validated', {
    event: validatedToken.webhook.event,
    jti: validatedToken.jti,
    issuer: validatedToken.iss
  });
} catch (error) {
  logWebhookEvent('webhook_validation_failed', {
    error: error.message,
    code: error.code,
    ip: req.ip,
    userAgent: req.get('User-Agent')
  }, 'error');
}

Metriken und Alerting #

const prometheus = require('prom-client');

// Metriken definieren
const webhookValidationCounter = new prometheus.Counter({
  name: 'webhook_validation_total',
  help: 'Total number of webhook validations',
  labelNames: ['status', 'error_type']
});

const webhookValidationDuration = new prometheus.Histogram({
  name: 'webhook_validation_duration_seconds',
  help: 'Duration of webhook validation',
  buckets: [0.1, 0.5, 1, 2, 5]
});

// Metriken verwenden
app.post('/webhook', async (req, res) => {
  const startTime = Date.now();
  
  try {
    const validatedToken = validateSWT(token, req.body, SECRET_KEY);
    
    webhookValidationCounter.inc({ status: 'success', error_type: 'none' });
    processWebhook(validatedToken.webhook, req.body);
    
    res.status(200).json({ status: 'success' });
  } catch (error) {
    webhookValidationCounter.inc({ 
      status: 'error', 
      error_type: error.code || 'unknown' 
    });
    
    res.status(error.statusCode || 500).json({
      error: error.code || 'UNKNOWN_ERROR',
      message: error.message
    });
  } finally {
    const duration = (Date.now() - startTime) / 1000;
    webhookValidationDuration.observe(duration);
  }
});

Rate Limiting #

const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 Minuten
  max: 100, // Maximal 100 Requests pro IP
  message: {
    error: 'RATE_LIMIT_EXCEEDED',
    message: 'Zu viele Webhook-Anfragen'
  },
  standardHeaders: true,
  legacyHeaders: false,
  skip: (req) => {
    // Authentifizierte Requests weniger limitieren
    const token = req.headers.authorization?.replace('Bearer ', '');
    try {
      jwt.verify(token, SECRET_KEY);
      return false; // Nicht skippen, aber weniger streng limitieren
    } catch {
      return false;
    }
  }
});

app.use('/webhook', webhookLimiter);