API-Sicherheit für B2B-SaaS: Mehr als OAuth und JWT

#API-Sicherheit B2B-SaaS
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Ihre SaaS-Anwendung hat OAuth 2.0 im Einsatz. JWTs transportieren die Nutzeridentität über Services hinweg. Der Login-Flow ist solide. Dann schickt Ihr erster Enterprise-Interessent seinen Vendor-Security-Fragebogen – vierzig Seiten an Kontrollen zu Zugriffsmanagement, Logging, Incident Response und Datenverarbeitung – und Sie stellen fest, dass OAuth und JWT genau zwei von vierzig Fragen beantwortet haben.

API-Sicherheit im B2B-Umfeld ist keine einzelne Technologie. Sie ist ein Bündel von Kontrollen, das Enterprise-Procurement-Teams, Security-Auditoren und Compliance-Frameworks in Kombination erwarten. JWT-Authentifizierung zu implementieren ist notwendig, aber bei weitem nicht ausreichend. Die Teams, die Enterprise-Security-Reviews konsequent bestehen, sind diejenigen, die Rate Limiting, API-Key-Lifecycle-Management, mandantenbezogene Autorisierung und umfassendes Audit-Logging als erstklassige Infrastrukturthemen begriffen haben – nicht als nachträgliche Ergänzung an einem laufenden Produkt.

Dieser Beitrag behandelt die Kontrollen, die für Enterprise-Kunden wirklich zählen, mit konkreter Implementierungsanleitung für PHP/Symfony-Backends und Next.js-API-Routen.

Rate Limiting: Die Kontrolle, die niemand implementiert, bis es zu spät ist

Rate Limiting liegt an einer interessanten Schnittstelle von Sicherheit, Zuverlässigkeit und Fairness. Aus Sicherheitssicht ist es Ihre erste Verteidigungslinie gegen Credential Stuffing, Brute-Force-Angriffe und Scraping. Aus Zuverlässigkeitssicht schützt es Ihre Infrastruktur davor, dass ein einzelner fehlerhafter Client den Service für alle verschlechtert. Aus Fairness-Sicht stellt es sicher, dass API-Kontingente so verteilt werden, wie es Ihr Preismodell vorsieht.

Enterprise-Kunden fragen nach Rate Limiting nicht, weil sie erwarten, Ihre Limits zu erreichen, sondern weil dessen Existenz operative Reife signalisiert. Eine API ohne Rate Limiting ist eine API, die eine außer Kontrolle geratene Integration oder ein falsch konfigurierter Client versehentlich lahmlegen kann – und dieses Risiko sind Enterprise-Procurement-Teams nicht bereit zu akzeptieren.

Wirksames Rate Limiting arbeitet auf mehreren Ebenen gleichzeitig. Limits auf Anwendungsebene (Requests pro Minute pro API-Key) erzwingen faire Nutzung. Limits auf Endpoint-Ebene (Authentifizierungsversuche pro IP) verhindern Credential-Angriffe. Burst-Limits (kurzzeitige Spitzen vor der Drosselung) tragen legitimen Integrationsmustern Rechnung, ohne normale Nutzung zu bestrafen.

In einer Symfony-Anwendung implementiert die RateLimiter-Komponente Token-Bucket- und Sliding-Window-Algorithmen mit Redis-gestütztem State:

// config/packages/rate_limiter.yaml
framework:
    rate_limiter:
        api_global:
            policy: sliding_window
            limit: 1000
            interval: '1 minute'
        api_auth:
            policy: token_bucket
            limit: 10
            rate: { interval: '1 minute', amount: 10 }
// src/EventListener/RateLimitListener.php
class RateLimitListener
{
    public function __construct(
        private RateLimiterFactory $apiGlobalLimiter,
        private RateLimiterFactory $apiAuthLimiter,
    ) {}

    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();
        $apiKey = $request->headers->get('X-API-Key', $request->getClientIp());

        $limiter = $this->apiGlobalLimiter->create($apiKey);
        $limit = $limiter->consume(1);

        if (!$limit->isAccepted()) {
            throw new TooManyRequestsHttpException(
                $limit->getRetryAfter()->getTimestamp() - time()
            );
        }
    }
}

Liefern Sie Retry-After-Header in 429-Antworten zurück. Enterprise-Integrationen sind darauf ausgelegt, sie zu respektieren – Clients, die sie ignorieren, sollte man kennen.

API-Key-Management: Rotation, Scoping und Widerruf

Viele SaaS-Produkte geben einen einzigen API-Key pro Account aus und betrachten das Thema als erledigt. Enterprise-Kunden arbeiten so nicht. Sie erwarten, dass API-Keys ohne Downtime rotierbar sind, auf bestimmte Berechtigungen beschränkt werden können und bei einem Sicherheitsvorfall sofort widerrufbar sind – ohne andere Keys desselben Accounts zu beeinträchtigen.

Ein robustes API-Key-Modell trennt den Key von den Berechtigungen, die er gewährt. Statt den API-Key selbst als Geheimnis zu behandeln, das Berechtigungen direkt kodiert, speichern Sie den Key als Referenz auf einen Berechtigungsdatensatz in Ihrer Datenbank:

// src/Entity/ApiKey.php
class ApiKey
{
    private string $id;          // UUID, stored in DB
    private string $keyHash;     // SHA-256 of the actual key — never store plaintext
    private string $keyPrefix;   // First 8 chars, shown in UI (e.g., "wt_live_a1b2c3d4")
    private array  $scopes;      // ["read:users", "write:webhooks", "admin:billing"]
    private ?\DateTimeImmutable $expiresAt;
    private ?\DateTimeImmutable $lastUsedAt;
    private ?\DateTimeImmutable $revokedAt;
    private string $tenantId;
    private string $createdByUserId;
}

Speichern Sie nur den Hash des tatsächlichen Key-Werts. Wenn ein Client einen Key präsentiert, hashen Sie ihn serverseitig und vergleichen ihn mit den gespeicherten Hashes. So legt ein Datenbank-Breach keine funktionierenden API-Credentials offen – das Muster entspricht der Passwortspeicherung, angewendet auf API-Keys.

Key-Rotation ohne Downtime erfordert die Unterstützung mehrerer gleichzeitig aktiver Keys pro Account während des Übergangsfensters. Ein Enterprise-Kunde, der Credentials einer Produktionsintegration rotiert, braucht den neuen Key funktionsfähig, bevor er den alten sicher deaktivieren kann. Gestalten Sie Ihre Key-Management-UI und -API so, dass Überlappungszeiträume von Tagen oder Wochen möglich sind, nicht von Sekunden.

Scopes verdienen eigene Aufmerksamkeit. Feingranulare Scopes erlauben Enterprise-Kunden, dem Prinzip der geringsten Privilegien zu folgen – die Integration, die Bestelldaten liest, sollte keinen Schreibzugriff auf Benutzerkonten haben. Dokumentieren Sie Ihre Scopes klar und erzwingen Sie sie auf Middleware-Ebene, nicht verstreut über einzelne Endpoint-Handler.

IP-Allowlisting: Einfach, auditierbar, von Enterprise-Kunden erwartet

IP-Allowlisting ist eine der ältesten Netzwerk-Sicherheitskontrollen und bleibt eine der am häufigsten von Enterprise-Procurement-Teams angefragten. Es liefert eine Defense-in-Depth-Schicht, die leicht zu verstehen, leicht zu auditieren und selbst bei kompromittiertem API-Key wirksam ist – ein gestohlener Key kann nicht von einer nicht autorisierten IP verwendet werden.

Die Implementierung ist unkompliziert. Speichern Sie erlaubte IP-Bereiche (einzelne Adressen ebenso wie CIDR-Notation für Unternehmens-VPNs) pro Account und validieren Sie eingehende Request-IPs auf Middleware-Ebene vor der Authentifizierung:

// src/Security/IpAllowlistChecker.php
class IpAllowlistChecker
{
    public function isAllowed(string $clientIp, string $tenantId): bool
    {
        $allowedRanges = $this->tenantRepository->getIpAllowlist($tenantId);

        // Empty allowlist means no restriction (opt-in feature)
        if (empty($allowedRanges)) {
            return true;
        }

        foreach ($allowedRanges as $range) {
            if ($this->ipInRange($clientIp, $range)) {
                return true;
            }
        }

        return false;
    }

    private function ipInRange(string $ip, string $range): bool
    {
        if (!str_contains($range, '/')) {
            return $ip === $range;
        }

        [$subnet, $prefix] = explode('/', $range);
        $mask = ~((1 << (32 - (int) $prefix)) - 1);

        return (ip2long($ip) & $mask) === (ip2long($subnet) & $mask);
    }
}

Ein operatives Detail: Vertrauen Sie dem richtigen IP-Header. Requests, die über einen Reverse Proxy eintreffen, tragen die echte Client-IP in X-Forwarded-For, nicht in REMOTE_ADDR. Symfonys Request::setTrustedProxies() behandelt das korrekt, wenn es mit dem IP-Bereich Ihres Proxys konfiguriert ist. Wer das falsch macht, erhält eine Allowlist, die legitime Clients blockiert und Angreifer durchlässt, die Header spoofen können – verifizieren Sie das in einer Staging-Umgebung, die Ihr Produktions-Proxy-Setup spiegelt.

Audit-Logging für API-Aufrufe: Was Enterprise-Kunden tatsächlich prüfen

Audit-Logging im Kontext von Änderungen auf Anwendungsebene haben wir in unserem Beitrag über SaaS-Architekturfehler in der Series A behandelt. Audit-Logging auf API-Ebene hat eine andere Form: Es muss jeden authentifizierten API-Aufruf mit genügend Kontext erfassen, um rekonstruieren zu können, was wann und von wo passiert ist – ohne Request- oder Response-Bodies zu loggen, die PII oder sensible Daten enthalten könnten.

Enterprise-Kunden, die Ihre Sicherheitskontrollen prüfen, wollen sehen, dass API-Aktivität unveränderlich geloggt wird, dass Logs für einen definierten Zeitraum aufbewahrt werden (typischerweise 12–24 Monate) und dass sie auf die Logs der Aktivitäten ihres eigenen Mandanten zugreifen können. Manche verlangen Log-Export in Standardformaten zur Einspeisung in ihr SIEM.

Der minimale brauchbare API-Audit-Log-Eintrag umfasst: Zeitstempel, API-Key-Kennung (nicht den Key selbst – die ID aus Ihrem Key-Management-System), Mandanten-ID, HTTP-Methode, Endpoint-Pfad (normalisiert, um Pfadparameter mit Nutzerdaten zu entfernen), Response-Statuscode, Response-Latenz und Client-IP. Bewusst nicht enthalten: Request-Bodies, Response-Bodies und Authorization-Header.

In Symfony erfasst ein Kernel-Response-Listener diese Daten nach Abschluss des Requests:

class ApiAuditListener
{
    public function onKernelResponse(ResponseEvent $event): void
    {
        $request  = $event->getRequest();
        $response = $event->getResponse();

        if (!$request->attributes->has('_api_key_id')) {
            return; // Not an authenticated API request
        }

        $this->auditLogger->info('api.call', [
            'api_key_id'  => $request->attributes->get('_api_key_id'),
            'tenant_id'   => $request->attributes->get('_tenant_id'),
            'method'      => $request->getMethod(),
            'path'        => $this->normalizePath($request->getPathInfo()),
            'status'      => $response->getStatusCode(),
            'latency_ms'  => (int) ((microtime(true) - $request->server->get('REQUEST_TIME_FLOAT')) * 1000),
            'client_ip'   => $request->getClientIp(),
            'user_agent'  => substr($request->headers->get('User-Agent', ''), 0, 200),
        ]);
    }

    private function normalizePath(string $path): string
    {
        // Replace UUIDs and numeric IDs with placeholders
        return preg_replace(
            ['/\/[0-9a-f]{8}-[0-9a-f-]{27}\//', '/\/\d+\//'],
            ['/{id}/', '/{id}/'],
            $path
        );
    }
}

Schreiben Sie Audit-Logs in einen Append-only-Speicher. In der Praxis bedeutet das entweder eine dedizierte Datenbanktabelle ohne DELETE- oder UPDATE-Rechte für den Anwendungsbenutzer oder die Weiterleitung an ein Log-Aggregationssystem wie OpenSearch oder ein verwaltetes SIEM. Logs, die von der Anwendung verändert werden können, sind keine Audit-Logs – sie sind nur Logs.

Mandantenbezogene Autorisierung in API-Routen

Für ein B2B-SaaS-Produkt mit mehreren Mandanten ist die gefährlichste Klasse von API-Sicherheitsfehlern der mandantenübergreifende Datenzugriff – wenn ein authentifizierter Request von Mandant A Daten von Mandant B lesen oder verändern kann. Das ist etwas anderes als Authentifizierung (zu beweisen, wer man ist) und Autorisierung auf Anwendungsebene (welche Aktionen man ausführen darf). Mandanten-Isolation ist eine dritte Schicht, die unabhängig durchgesetzt werden muss.

Das Fehlermuster ist subtil. Ein API-Endpoint, der eine Ressourcen-ID im URL-Pfad akzeptiert – GET /api/orders/{orderId} – validiert vielleicht korrekt, dass der Aufrufer authentifiziert ist und den Scope read:orders besitzt. Aber wenn er die Datenbank per ID abfragt, ohne zu prüfen, ob die Bestellung zum Mandanten des Aufrufers gehört, kann ein Aufrufer, der Bestell-IDs errät oder durchprobiert, fremde Daten lesen.

Die Lösung ist, immer auf der Datenzugriffsebene nach Mandanten-ID zu filtern, nicht auf Controller-Ebene. In einer Symfony-Anwendung lässt sich das über Doctrines globalen Filtermechanismus durchsetzen (beschrieben in unserem Leitfaden zur Multi-Tenant-Architektur) oder über ein Repository-Pattern, das die Mandanten-Bedingung immer einschließt:

// src/Repository/OrderRepository.php
class OrderRepository extends ServiceEntityRepository
{
    public function findForTenant(string $orderId, string $tenantId): ?Order
    {
        return $this->findOneBy([
            'id'       => $orderId,
            'tenantId' => $tenantId,  // Always scoped — cannot be omitted
        ]);
    }
    // No findById() method — forces callers to provide tenantId
}

Indem findById() nicht verfügbar ist und Aufrufer findForTenant() verwenden müssen, macht das Repository die sichere Wahl zur einzigen Wahl. Ein Endpoint, der vergisst, die Mandanten-ID zu übergeben, scheitert zur Compile-Zeit oder früh im Testing – nicht stillschweigend in Produktion.

In einer Next.js-API-Route gilt dasselbe Prinzip. Die Middleware sollte den Mandanten-Kontext auflösen und an den Request anhängen, bevor ein Route-Handler ausgeführt wird:

// middleware.ts — resolve tenant from API key, attach to request
export async function middleware(request: NextRequest) {
  const apiKey = request.headers.get('x-api-key');
  if (!apiKey) return new Response('Unauthorized', { status: 401 });

  const keyRecord = await resolveApiKey(apiKey); // hashes key, looks up record
  if (!keyRecord || keyRecord.revokedAt) {
    return new Response('Unauthorized', { status: 401 });
  }

  const headers = new Headers(request.headers);
  headers.set('x-tenant-id', keyRecord.tenantId);
  headers.set('x-api-key-id', keyRecord.id);
  headers.set('x-api-scopes', keyRecord.scopes.join(','));

  return NextResponse.next({ request: { headers } });
}

Route-Handler lesen x-tenant-id aus den verarbeiteten Request-Headern und übergeben es an jeden Datenzugriff. Weil die Mandanten-ID aus dem verifizierten Key-Datensatz stammt – nicht aus einem vom Aufrufer gelieferten Query-Parameter – können Mandanten ihre eigenen Requests nicht auf die Daten eines anderen Mandanten richten.

Alles zusammengefügt: Die Security-Control-Matrix

Enterprise-Security-Fragebögen sind leichter zu beantworten, wenn Sie Ihre Kontrollen den Standard-Frameworks zuordnen können, auf die sie verweisen. Die obigen Kontrollen lassen sich direkt auf gängige Audit-Kategorien abbilden:

Rate Limiting adressiert Verfügbarkeit und Resilienz gegen Denial-of-Service. API-Key-Rotation und -Widerruf adressieren Credential-Lifecycle-Management, eine Kernanforderung in den Access-Control-Domänen von ISO 27001 und SOC 2. IP-Allowlisting bietet eine Zugriffsbeschränkung auf Netzwerkebene, die viele Kunden aus dem Finanzsektor vertraglich verlangen. Audit-Logging adressiert die Monitoring- und Logging-Anforderungen praktisch jedes Compliance-Frameworks. Mandanten-Isolation adressiert Datentrennung und Zugriffskontrolle.

Keine dieser Kontrollen ist für sich genommen besonders komplex zu implementieren. Die Herausforderung besteht darin, sie als kohärente Infrastruktur umzusetzen – konsistent angewendet, operativ überwachbar und nachweislich funktionierend – bevor der erste Enterprise-Deal in Ihrer Pipeline ist, nicht danach.

Ein Code-Quality-Audit einer B2B-SaaS-API prüft gezielt, ob diese Kontrollen konsistent über alle Endpoints angewendet werden oder ob Lücken in Randfällen und selten genutzten Codepfaden bestehen. Ein Review der Tech-Stack-Strategie kann helfen zu priorisieren, welche Kontrollen angesichts Ihrer aktuellen Kundenbasis und der Compliance-Anforderungen Ihrer Ziel-Branchen zuerst umgesetzt werden sollten.

Sicherheit ist eine Haltung, kein Feature

OAuth und JWT sind die Eintrittskarte ins Enterprise-Gespräch. Sie zeigen, dass Sie über Authentifizierung nachgedacht haben. Die in diesem Beitrag behandelten Kontrollen sind das, was Sie in diesem Gespräch hält – und was letztlich entscheidet, ob Enterprise-Procurement-Teams den Deal freigeben oder Sie bitten wiederzukommen, wenn die Sicherheitskontrollen reifer sind.

Die gute Nachricht: Diese Kontrollen sind gut verstanden, inkrementell implementierbar und erfordern keinen Neubau Ihrer Anwendung. Die Teams, die Enterprise-Security-Reviews am reibungslosesten durchlaufen, sind diejenigen, die diese Kontrollen von Anfang an als Infrastruktur behandelt haben – statt als Features, die unter Termindruck nachgerüstet werden.

Wenn Sie sich auf Ihr erstes Enterprise-Security-Review vorbereiten oder eine Codebasis geerbt haben, die eine Bewertung der Sicherheitslage braucht, bietet Wolf-Tech ein fokussiertes API-Security-Review als Teil unserer Code-Quality-Consulting-Praxis an. Kontaktieren Sie uns unter hello@wolf-tech.io oder besuchen Sie wolf-tech.io, um zu besprechen, was ein Review für Ihren spezifischen Stack abdecken würde.