← Zurück

Kapitelkompass

Zugriff als nachvollziehbare Entscheidung

Authentifizierung liefert Identität; Autorisierung bewertet Kontext, Ressource, Mandant und konkrete Berechtigung.

IdentitätAusgangspunktPolicyEinordnenEntscheidungAbsichernAuditErgebnis
Der Themenweg zeigt die fachliche Leserichtung dieses Kapitels.

Das nimmst du mit

  • AuthN und AuthZ trennen
  • Policies explizit machen
  • Mandanten isolieren
  • Audit beweiskräftig halten

Praxisfall

Ein Support-Mitarbeiter darf Metadaten sehen, aber keine Zahlungsdetails eines fremden Mandanten.

Entscheidung

Berechtigungen an fachlichen Aktionen ausrichten, nicht an zufälligen Controller-Methoden.

!

Typisches Risiko

Breite ADMIN-Rollen umgehen Ownership und erschweren spätere Least-Privilege-Regeln.

07. Security, Rollen, Permissions und Audit

Security Deep Dive: Authentifizierung, Autorisierung, Rollen, Permissions, OAuth2/OIDC-Mentalmodell, JWT, Service-to-Service, Audit und sichere Fehlerantworten.

1. Warum Security im Enterprise-System selten nur Login ist

In einfachen Tutorials bedeutet Security oft: Benutzer meldet sich an, danach wird ein Controller mit @RolesAllowed("ADMIN") geschuetzt. In echten Enterprise-Systemen reicht das fast nie. Eine Order-Plattform muss unterscheiden, ob ein Kunde seine eigene Bestellung liest, ob Support fremde Bestellungen lesen darf, ob Billing Rechnungen stornieren darf, ob ein Payment-Service Zahlungen autorisieren darf und ob Audit-Logs nur fuer Revisionsrollen sichtbar sind.

Das zentrale Missverstaendnis lautet: Authentifizierung ist nicht Autorisierung. Authentifizierung sagt, wer der Akteur ist. Autorisierung sagt, ob dieser Akteur diese konkrete Aktion auf dieser konkreten Ressource ausfuehren darf. Eine Rolle ist dabei nur ein grober Baustein. Die eigentliche Entscheidung braucht meistens Ressource, Mandant, Besitzer, Status, Scope, Zeit, Risiko und Aktion.

Schlechte naive Loesung

JAVA
if (user.role().equals("ADMIN")) {
    return orderRepository.findById(orderId);
}
throw new ForbiddenException();

Diese Loesung wirkt einfach, aber sie ist fachlich zu grob. Sie beantwortet nicht, ob der Kunde seine eigene Bestellung sehen darf, ob Support nur lesen, aber nicht stornieren darf, ob Billing andere Rechte braucht und ob ein fremder Mandant versteckt werden muss.

Bessere Denkweise

JAVA
AccessDecision decision = orderReadPolicy.evaluate(securityContext, order);

Die Policy sieht den Akteur, die Ressource und die Aktion. Dadurch kann sie entscheiden: Kunde liest eigene Bestellung, Support liest beliebige Bestellung, fremder Mandant wird als 404 Not Found verborgen, Admin bekommt Sonderrechte.

2. Authentifizierung: Identitaet, Claims und SecurityContext

Authentifizierung besteht nicht nur aus dem Pruefen eines Tokens. Ein Enterprise-System muss einen technischen Nachweis in einen fachlichen Kontext umwandeln. Dieser Kontext enthaelt Subjekt, Tenant, Rollen, Scopes, Authentifizierungszeitpunkt, Remote-Adresse und Correlation ID.

JAVA
public record SecurityPrincipal(
String subject,
String tenantId,
Set<Role> roles,
Set<String> scopes,
Instant authenticatedAt
) {
}

Der SecurityContext ist bewusst fachlich. Controller, Application Services und Policies sollen nicht direkt mit rohen JWT-Strings arbeiten. Token sind Transportartefakte. Fachliche Sicherheitsentscheidungen brauchen normalisierte Akteure.

Token-Validierungspipeline

Eine robuste Validierung prueft mindestens:

Didaktischer Code im Lab

Das Lab verwendet bewusst einen kleinen HMAC-basierten Token-Service. Das ist kein Produktions-JWT-Framework, sondern ein Lernmodell. Es macht sichtbar, welche Schritte ein Framework spaeter fuer dich uebernimmt.

JAVA
TokenClaims claims = tokenService.validate(token, clock.instant());
SecurityPrincipal principal = new SecurityPrincipal(
claims.subject(), claims.tenantId(), claims.roles(), claims.scopes(), clock.instant());

Wichtig: In Produktion sollte man etablierte JOSE/OIDC-Bibliotheken und die Security-Funktion des Frameworks verwenden. Die fachliche Entscheidung bleibt trotzdem deine Aufgabe.

3. RBAC, Permissions, Ownership und Tenant-Grenzen

RBAC ist gut fuer grobe Verantwortungen. Permissions sind besser fuer konkrete Aktionen. Ownership ist notwendig, wenn Benutzer eigene Ressourcen sehen oder aendern duerfen. Tenant-Grenzen verhindern, dass Daten zwischen Mandanten sichtbar werden.

Beispielrollen

JAVA
public enum Role {
    CUSTOMER(EnumSet.of(Permission.ORDER_READ_OWN, Permission.ORDER_CANCEL_OWN)),
    SUPPORT(EnumSet.of(Permission.ORDER_READ_ANY)),
    BILLING(EnumSet.of(Permission.INVOICE_READ_ANY, Permission.INVOICE_CANCEL)),
    PAYMENT_SERVICE(EnumSet.of(Permission.PAYMENT_AUTHORIZE)),
    AUDITOR(EnumSet.of(Permission.AUDIT_READ)),
    ADMIN(EnumSet.of(Permission.ADMIN_ALL));
}

Eine Rolle wie SUPPORT darf in diesem Modell Bestellungen lesen, aber nicht automatisch Zahlungen ausloesen oder Rechnungen stornieren. Das verhindert Rechteausweitung durch zu breite Rollen.

Ownership-Regel

JAVA
boolean isOwner = principal.subject().equals(order.owner().value());
if (isOwner && principal.hasPermission(Permission.ORDER_READ_OWN)) {
    return AccessDecision.allowed("owner grants ORDER_READ_OWN");
}

Diese Regel ist fachlich: Der Kunde darf seine eigene Bestellung lesen. Das ist nicht dasselbe wie eine globale ORDER_READ_ANY-Berechtigung.

Tenant-Grenze

JAVA
if (!principal.tenantId().equals("tenant-main")) {
    return AccessDecision.denied("wrong tenant", DenialVisibility.HIDE_AS_NOT_FOUND);
}

Ein fremder Tenant bekommt bewusst nicht 403, sondern kann als 404 verborgen werden. Das reduziert Information Disclosure: Der Client lernt nicht, ob die Ressource existiert.

4. Policy statt versteckte Controller-Logik

Autorisierung in Controllern wird schnell unwartbar. Heute steht dort if support, morgen kommt Tenant-Pruefung, danach Status-Regel, danach Audit und Sonderfall fuer Compliance. Besser ist eine ausdrueckliche Policy.

JAVA
public interface AccessPolicy<T> {
    AccessDecision evaluate(SecurityContext context, T resource);
}

Die Policy ist testbar. Sie kann in REST, Messaging, Batchjobs und internen Services wiederverwendet werden. Sie entkoppelt Security-Regeln von Transporttechnik.

Policy-Entscheidung als fachliches Ergebnis

JAVA
public sealed interface AccessDecision permits AccessDecision.Allowed, AccessDecision.Denied {
    record Allowed(String reason) implements AccessDecision {
    }
record Denied(String reason, DenialVisibility visibility) implements AccessDecision {
}
}

Der Grund der Entscheidung ist fuer Audit wichtig. Die Sichtbarkeit ist fuer sichere Fehlerantworten wichtig. Nicht jede Ablehnung sollte dem Client gleich viel verraten.

5. Sichere Fehlerantworten: 401, 403, 404

Security-Fehler sind nicht nur HTTP-Statuscodes. Sie sind ein Teil des Sicherheitsdesigns.

Status Bedeutung Typischer Fall Vorsicht
401 nicht authentifiziert Token fehlt, ungueltig oder abgelaufen keine Signaturdetails leaken
403 authentifiziert, aber nicht erlaubt Benutzer ist bekannt, Permission fehlt keine internen Policy-Namen leaken
404 Ressource nicht sichtbar oder nicht vorhanden fremder Tenant, fremde private Ressource bewusst als Schutz gegen Enumeration

Problem Details

JAVA
return new ProblemDetails(
403,
"https://errors.example.com/forbidden",
"Forbidden",
"You are not allowed to perform this action.",
correlationId,
Map.of());

Die Antwort ist stabil, knapp und sicher. Interne Gruende bleiben im Audit, nicht in der API-Antwort.

6. Audit: Jede relevante Security-Entscheidung muss rekonstruierbar sein

Audit ist nicht nur Logging. Ein Audit-Ereignis muss spaeter beantworten koennen: Wer hat was auf welcher Ressource versucht, wann, mit welcher Entscheidung und welcher Correlation ID?

JAVA
public record AuditEvent(
Instant occurredAt,
String correlationId,
String actor,
String action,
String resource,
String decision,
String reason,
String remoteAddress
) {
}

Was nicht ins Audit gehoert

Was ins Audit gehoert

7. Service-to-Service Security

In Enterprise-Systemen sprechen Services miteinander. Ein User-Token ist nicht automatisch ein Service-Token. Ein Payment-Service, der eine Zahlung autorisiert, braucht eigene Identitaet, eigene Scopes und eigene Auditierbarkeit.

Typische Varianten

Variante Zweck Risiko
User Token weiterreichen User-Kontext bleibt erhalten nicht jeder Service darf alles mit User-Rechten tun
On-behalf-of Service handelt kontrolliert fuer Benutzer komplexere Token- und Scope-Regeln
Client Credentials Service handelt als Service kein User-Kontext, Audit braucht Service-Actor
mTLS technische Service-Identitaet ersetzt nicht fachliche Autorisierung

Scope Policy

JAVA
public final class ServiceTokenPolicy implements AccessPolicy<String> {
    private final String requiredScope;
    public AccessDecision evaluate(SecurityContext context, String action) {
        return context.principal()
        .filter(principal -> principal.hasScope(requiredScope))
        .map(principal -> AccessDecision.allowed("service scope grants " + requiredScope))
        .orElseGet(() -> AccessDecision.denied("missing service scope " + requiredScope,
        DenialVisibility.SHOW_FORBIDDEN));
    }
}

Scopes beschreiben technische API-Rechte. Sie sollten nicht blind mit fachlichen Permissions gleichgesetzt werden.

8. Security im Zusammenspiel mit REST

Aus Kapitel kommt der API-Vertrag. Kapitel fuegt die Sicherheitsentscheidung hinzu. Der Request-Flow wird damit:

TEXT
HTTP Request
  -> Correlation ID
  -> Token aus Authorization Header
  -> Token validieren
  -> SecurityContext erzeugen
  -> DTO validieren
  -> Ressource laden
  -> Policy pruefen
  -> Use Case ausfuehren
  -> Audit schreiben
  -> sichere Response erzeugen

Wichtig ist die Reihenfolge. Manchmal muss eine Ressource geladen werden, bevor Ownership geprueft werden kann. Dabei darf aber kein Datenleck entstehen. Repository-Zugriff, Policy und Fehlerabbildung muessen gemeinsam geplant werden.

9. Typische Fehler und Anti-Patterns

Anti-Pattern: Rollen als Strings quer im Code

TEXT
if (role.equals("ADMIN") || role.equals("SUPER_USER")) { ... }

Problem: schwer testbar, schwer refactorbar, keine zentrale Permission-Sicht.

Anti-Pattern: Security nur im Controller

Wenn Messaging-Consumer oder Batchjobs dieselbe Use-Case-Logik ohne Controller ausfuehren, werden Regeln umgangen.

Anti-Pattern: 403 fuer fremde private Ressourcen

Ein Angreifer kann IDs raten und durch 403 erkennen, dass eine Ressource existiert.

Anti-Pattern: Token-Claims blind vertrauen

Claims muessen zu Issuer, Audience, Ablaufzeit, Tenant und Signatur passen. Sonst akzeptiert das System fremde oder abgelaufene Tokens.

Anti-Pattern: Audit erst nach erfolgreicher Aktion

Auch verweigerte Zugriffe sind sicherheitsrelevant.

10. Teststrategie fuer Security-Regeln

Security braucht eigene Tests. Controller-Tests allein sind zu grob. Gute Security-Tests pruefen Policies, Fehlerabbildung, Audit und Service-Scopes.

Im Lab enthalten:

TEXT
- Owner darf eigene Bestellung lesen
- fremder Kunde bekommt NotFound
- Support darf beliebige Bestellung lesen
- ungueltiges Token wird 401
- abgelaufenes Token wird 401
- Allow und Deny erzeugen Audit Events
- Service-Scope erlaubt Payment-Aktion

Beispieltest

JAVA
SecurityContext stranger = f.context("customer-2", Set.of(Role.CUSTOMER), Set.of("orders:read"), 3600);
assertThrows(NotFoundException.class,
() -> f.query.getOrder(stranger, new OrderId("ORD-1")),
"foreign customer hidden as not found");

Dieser Test ist fachlich wichtiger als ein reiner HTTP-Test. Er prueft die echte Regel: Fremde Ressource wird verborgen.

11. Produktionshinweise

12. Checkliste

TEXT
[ ] Gibt es eine klare Trennung zwischen Authentifizierung und Autorisierung?
[ ] Sind Rollen, Permissions, Scopes und Ownership sauber getrennt?
[ ] Sind Tenant-Grenzen testbar?
[ ] Gibt es Policies statt verstreuter if-Abfragen?
[ ] Werden Deny-Entscheidungen auditiert?
[ ] Leaken Fehlerantworten keine internen Details?
[ ] Werden Service-Identitaeten getrennt von User-Identitaeten behandelt?
[ ] Gibt es Tests fuer Owner, Support, Billing, Service und fremde Tenants?
[ ] Ist die Security-Logik ausserhalb des Controllers wiederverwendbar?

13. Code-Walkthrough

Der wichtigste Pfad im Lab ist:

TEXT
SecurityContextFactory
  -> SignedTokenService.validate
  -> OrderQueryService.getOrder
  -> AuthorizationService.require
  -> OrderReadPolicy.evaluate
  -> AuditSink.append
  -> SecurityExceptionMapper.toProblem

Die Architekturabsicht ist bewusst: Token-Technik ist am Rand, Policy ist im Application-Bereich, Audit ist ein Port, Fehlerantworten werden gemappt.

14. Lernaufgaben

  1. Erweitere OrderCancelPolicy: Kunde darf eigene Bestellung nur stornieren, wenn Status NEW oder CONFIRMED ist.
  2. Fuege eine TenantPolicy hinzu, die Tenant-Pruefungen zentralisiert.
  3. Erweitere Audit um clientId fuer Service-Accounts.
  4. Schreibe einen Test, der verhindert, dass TokenValidationException-Details in Problem Details auftauchen.
  5. Modellieren SUPPORT_READ_ONLY und SUPPORT_MANAGER als getrennte Rollen.

15. Zusammenfassung

Kapitel vertieft Security nicht als Framework-Konfiguration, sondern als Architekturentscheidung. Authentifizierung liefert Identitaet. Autorisierung prueft konkrete Aktionen auf konkrete Ressourcen. Policies machen Regeln sichtbar. Audit macht Entscheidungen rekonstruierbar. Sichere Fehlerantworten verhindern Informationslecks. Service-to-Service-Security trennt technische Dienstidentitaet von Benutzeridentitaet.

⌂ Cockpit