Authentifizierung ist nicht Autorisierung
Diese beiden Begriffe werden oft synonym verwendet, aber sie bedeuten nicht dasselbe. Authentifizierung beantwortet die Frage „Wer bist du?“ und endet mit einem vertrauenswürdigen Principal: einer User-ID, einem Tenant und einem Weg, zu verifizieren, dass die Anfrage von dieser Person stammt. Autorisierung beantwortet die Frage „Was darfst du tun?“ und wird nach der Authentifizierung bei jeder Anfrage gegen ein spezifisches Ziel ausgeführt.
Die Verwechslung dieser beiden Konzepte ist die Ursache für einige der häufigsten Sicherheitslücken im Web. Ein perfekt implementierter Login sagt nichts darüber aus, ob der angemeldete Benutzer die Rechnung eines anderen Kunden lesen darf. Eine gültige Session beweist die Identität; sie gewährt jedoch keine Rechte. Jeder Endpoint muss immer noch explizit entscheiden, ob dieser Principal diese Aktion an dieser Ressource ausführen darf.
In diesem Guide geht es um die zweite Frage. Es wird vorausgesetzt, dass die erste bereits gelöst ist – also eine Session oder ein Token vorliegt, aus dem ein Benutzer hervorgeht. Der Fokus liegt auf dem Modell und den Prüfungen, die diesen Benutzer in ein „Erlaubt“ oder „Abgelehnt“ verwandeln. Falls die Authentifizierung noch offen ist, lies zuerst den Guide zu Session-Auth.
Das RBAC-Modell
Role-Based Access Control ist das am weitesten verbreitete Autorisierungsmodell, da es der tatsächlichen Denkweise in Organisationen entspricht. Es gibt drei Kernkonzepte:
- Principals sind die handelnden Einheiten: Benutzer, Service-Accounts, API-Keys. Jeder Principal gehört zu einem Tenant.
- Roles sind benannte Bündel von Fähigkeiten:
viewer,editor,admin,billing. - Permissions sind die kleinsten Einheiten (Atome): eine einzelne Aktion auf einem einzelnen Ressourcentyp, geschrieben als
posts:updateoderbilling:read.
Einem Principal werden eine oder mehrere Roles zugewiesen, und jede Role ist auf eine Menge von Permissions gemappt. Die effektive Berechtigungsmenge für einen Request ist die Vereinigung aller Permissions aus allen Roles des Principals. Eine Autorisierungsprüfung reduziert sich dann auf einen einfachen Test der Mengenmitgliedschaft, ergänzt um etwaige ressourcenspezifische Regeln.
Die Eleganz liegt darin, dass Permissions stabil bleiben, während Roles flexibel sind. Sie können eine moderator-Role hinzufügen, posts:delete darin verschieben, und kein einziger Handler muss geändert werden. Die Regel „wer darf einen Post löschen“ existiert in den Daten und nicht in einer Kette von if-Statements, die über die gesamte Codebase verteilt sind.
Benutzer, Rollen und Berechtigungen
Die relationale Struktur besteht aus vier Tabellen und zwei Many-to-Many-Joins. Es lohnt sich, dieses Konzept zu verinnerlichen, da fast jede RBAC-Implementierung eine Variation davon ist.
CREATE TABLE permissions (
id bigserial PRIMARY KEY,
action text NOT NULL UNIQUE
);
CREATE TABLE roles (
id bigserial PRIMARY KEY,
name text NOT NULL UNIQUE
);
CREATE TABLE role_permissions (
role_id bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
permission_id bigint NOT NULL REFERENCES permissions (id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
role_id bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);
Es ist wichtig, diese Daten in einer Migration zu seeden. Berechtigungen und Rollenzuweisungen sind Teil des Vertrags Ihrer Anwendung und nichts, was ein Administrator in der Produktion improvisiert. Behalten Sie den Seed in der Versionsverwaltung, damit jede Umgebung die gleiche Definition von editor verwendet, und behandeln Sie Änderungen daran mit der gleichen Sorgfalt wie eine Schema-Änderung.
Das Laden der effektiven Berechtigungen erfolgt über eine einzige Query:
SELECT DISTINCT p.action
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE ur.user_id = $1;
Cachen Sie das Ergebnis pro Request. Wenn Sie die Daten einmal laden und an req.user anhängen, vermeiden Sie es, die Query bei jeder Prüfung zu wiederholen. Ein Cache mit kurzer TTL, der über die User-ID indiziert ist, hält die Datenbank aus dem Hot Path heraus.
Rollenhierarchien
Echte Organisationen haben Ebenen. Eine Senior-Rolle kann normalerweise alles, was eine Junior-Rolle kann, und noch mehr. Dies zu modellieren, indem man jede Berechtigung in jede Rolle kopiert, ist eine Wartungsfalle: Ändert man posts:read, muss man an alle fünf Rollen denken, die diese enthalten.
Lassen Sie Rollen stattdessen vererben. Fügen Sie eine parent_role_id oder eine role_inherits Join-Tabelle hinzu und erweitern Sie die Hierarchie beim Aufbau des Berechtigungssatzes. Eine gängige Struktur ist viewer → editor → admin, bei der jede Ebene zusätzliche Funktionen hinzufügt.
CREATE TABLE role_inherits (
role_id bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
parent_id bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
PRIMARY KEY (role_id, parent_id)
);
Die Erweiterung erfolgt über eine rekursive Abfrage oder, einfacher gesagt, über eine vorberechnete Closure-Tabelle, die jedes Vorfahren-Paar speichert. Die Closure-Tabelle tauscht etwas Speicherplatz gegen einen trivialen, indexfreundlichen Lookup ein, was in der Regel die richtige Entscheidung ist, da Berechtigungsprüfungen weitaus häufiger vorkommen als Rollenänderungen.
Schützen Sie sich vor Zyklen. Eine Rolle, die direkt oder über eine Kette von sich selbst erbt, führt dazu, dass die Erweiterung in einer Endlosschleife landet. Validieren Sie beim Schreiben: Lehnen Sie jeden Parent ab, der einen Zyklus erzeugen würde. Halten Sie Hierarchien flach – drei oder vier Ebenen sind völlig ausreichend –, da tiefe Bäume für Menschen schwer nachvollziehbar sind und leicht zu Fehlern führen.
Warum Berechtigungen besser sind als Rollen-Strings
Der häufigste Fehler bei RBAC besteht darin, gar kein echtes RBAC aufzubauen. Stattdessen werden Rollenprüfungen wahllos über die gesamte Codebasis verteilt:
if (req.user.role !== "admin") return res.sendStatus(403);
Das sieht harmlos aus und ist eine Richtlinienentscheidung, die direkt in einen Handler eingebettet ist. Es besagt, dass nur admin dies tun darf – was zum Zeitpunkt des Schreibens vielleicht korrekt war. Wenn das Produkt jedoch eine support-Rolle einführt, die ebenfalls Zugriff benötigt, muss jemand jede einzelne dieser Prüfungen finden und bearbeiten – und dabei wird garantiert eine übersehen. Die Regel ist nun über Dutzende von Dateien verstreut, ohne dass es eine einzige „Source of Truth“ gibt.
Die Prüfung einer Berechtigung kehrt diese Abhängigkeit um. Der Handler fragt: „Darf dieser Principal einen Post aktualisieren?“, und die Antwort kommt aus den Daten:
if (!can(req.user, "update", post)) return res.sendStatus(403);
Die Fähigkeit, Posts zu aktualisieren, der Rolle support zuzuweisen, ist nun lediglich ein Eintrag in role_permissions und keine Code-Änderung mehr. Die Richtlinie ist überprüfbar, testbar und konsistent. Der Handler beschreibt die Absicht, anstatt eine spezifische Rolle zu kodieren.
Die Faustregel lautet: Rollen sind für Menschen, Berechtigungen sind für den Code. Eine UI mag sagen: „Admins können die Abrechnung verwalten“, aber die darunterliegende Prüfung sollte nach billing:manage fragen.
Die Autorisierungs-Pipeline
Jeder Request durchläuft dieselbe Sequenz, wobei jede Stufe genau eine Aufgabe hat.
- Authentifizierung. Die Session oder das Token wird in einen Principal aufgelöst: eine User-ID, eine Tenant-ID und eine Rollenliste. Schlägt dies fehl, gilt der Request als anonym und geschützte Routen geben einen 401 zurück.
- Rollen laden. Die Rollenzuweisungen werden abgerufen, üblicherweise aus der Datenbank oder einem Cache, der beim Login befüllt wurde.
- In Berechtigungen auflösen. Rollen, einschließlich vererbter Rollen, werden in einen einzigen Satz von Permission-Strings abgeflacht.
- Aktion an der Ressource prüfen. Rufen Sie
can(user, action, resource)für das konkrete Ziel auf, nachdem dieses geladen wurde. - Zulassen oder Ablehnen. Bei Erfolg wird der Handler ausgeführt. Bei einem Fehler wird ein 403 ohne Seiteneffekte zurückgegeben.
- Entscheidung protokollieren. Der Principal, die Aktion, die Ressource und das Ergebnis werden aufgezeichnet.
Die Reihenfolge ist aus zwei Gründen entscheidend. Die Authentifizierung muss zuerst erfolgen, da alles Weitere von einem vertrauenswürdigen Principal abhängt. Ressourcenprüfungen müssen nach dem Laden der Ressource erfolgen, da man das Eigentum an einem Datensatz nicht prüfen kann, den man noch nicht abgerufen hat.
Ein „Fail-Closed“-Verhalten ist nicht verhandelbar. Wenn das Laden der Rollen einen Fehler wirft oder der Cache nicht erreichbar ist, lautet die Standardantwort „deny“. Ein Autorisierungssystem, das bei einem Fehler „allow“ zurückgibt, ist schlimmer als gar kein System, da es ein falsches Sicherheitsgefühl vermittelt.
Einen can()-Helper erstellen
Die Zentralisierung der Entscheidung in einer einzigen Funktion verhindert, dass Route Guards und Ressourcenprüfungen auseinanderdriften. Die Signatur ist simpel: ein Principal, eine Action und eine optionale Resource.
export type Action = "read" | "create" | "update" | "delete" | "manage";
export function can(
user: Principal,
action: Action,
resource?: Resource
): boolean {
const permission = `${resource?.type ?? "global"}:${action}`;
if (!user.permissions.has(permission)) return false;
if (resource && resource.tenantId !== user.tenantId) return false;
if (resource && action !== "read" && resource.ownerId !== user.id) {
return user.permissions.has(`${resource.type}:manage`);
}
return true;
}
Hier sind drei Regeln kodiert, geordnet nach ihrer Wichtigkeit. Das Permission-Set ist das grobe Filterelement: Wenn keine Rolle die Action erlaubt, wird der Vorgang gestoppt. Danach folgt die Tenant-Isolation, die absolut ist – ein Principal darf niemals außerhalb seines Tenants agieren, unabhängig von den Berechtigungen. Schließlich erfordern Schreibzugriffe auf eine Resource entweder Ownership oder einen expliziten manage-Grant; dies ermöglicht es beispielsweise einem Editor, eigene Entwürfe zu bearbeiten, während ein Admin alles bearbeiten kann.
Der Helper ist “pure”. Er nimmt einfache Daten entgegen und gibt einen Boolean zurück, ohne interne Datenbankaufrufe zu tätigen. Dadurch ist es trivial, ihn mit einer Matrix aus Principals, Actions und Resources mittels Unit-Tests zu prüfen. Zudem bedeutet es, dass dieselbe Funktion in einem Route Guard, einem Service, einem Background Job oder einer UI-Komponente laufen kann, die entscheidet, ob ein Button gerendert werden soll.
Für die UI stellen Sie dieselbe Funktion über einen Endpoint oder ein server-gerendertes Permissions-Objekt für den Client bereit. Der Client sollte Steuerelemente ausblenden, die der Benutzer nicht verwenden kann, aber der Server muss dennoch jede Prüfung erzwingen, da ein ausgeblendeter Button kein Sicherheitsmechanismus ist.
Erzwingung auf Route-Ebene
Ein Route Guard ist die erste Verteidigungslinie: Er entscheidet, ob diese Art von Aktion für diesen Principal überhaupt verfügbar ist. Er wird vor dem Handler und vor jeglichen Datenbankoperationen ausgeführt, was ihn zu einem effizienten Weg macht, offensichtliche Ablehnungen schnell abzuweisen.
export function requirePermission(
action: Action,
type: string
): RequestHandler {
return (req, res, next) => {
if (!req.user) return res.status(401).json({ error: "unauthorized" });
if (!can(req.user, action, { type, ownerId: req.user.id, tenantId: req.user.tenantId })) {
return res.status(403).json({ error: "forbidden" });
}
next();
};
}
Binden Sie ihn an den Router, damit die Regel dort sichtbar ist, wo die Routes definiert werden:
router.get("/posts", requirePermission("read", "post"), listPosts);
router.post("/posts", requirePermission("create", "post"), createPost);
Es ist wichtig, bei einem fehlenden Principal einen 401- und bei einem abgelehnten Principal einen 403-Statuscode zurückzugeben. 401 bedeutet „Ich weiß nicht, wer Sie sind“; 403 bedeutet „Ich weiß, wer Sie sind, aber Sie dürfen dies nicht tun“. Clients und Monitoring-Tools behandeln diese unterschiedlich, und eine Vermischung erschwert das Debugging.
Route Guards sind notwendig, aber nicht ausreichend. Sie beantworten die Frage „Darf dieser Principal generell Posts aktualisieren?“, nicht aber „Darf er Post 42 aktualisieren?“. Letztere Frage erfordert den Zugriff auf die Ressource.
Erzwingung auf Ressourcenebene
Die Prüfung auf Ressourcenebene ist der Ort, an dem die meisten echten Schwachstellen gefunden werden, da sie oft vergessen wird. Ein Endpunkt wie PATCH /posts/:id empfängt eine id vom Client. Wenn diese id ohne eine Prüfung des Eigentums vertraut wird, kann jeder authentifizierte Benutzer jeden Beitrag ändern, indem er ids errät oder aufzählt. Dies ist eine Insecure Direct Object Reference, oder IDOR.
Die Lösung folgt immer demselben Muster: Lade die Ressource und prüfe sie anschließend.
router.patch("/posts/:id", requireAuth(), async (req, res) => {
const post = await db.post.findById(req.params.id);
if (!post) return res.status(404).json({ error: "not_found" });
const allowed = can(req.user!, "update", {
type: "post",
ownerId: post.authorId,
tenantId: post.tenantId,
});
if (!allowed) return res.status(403).json({ error: "forbidden" });
const updated = await db.post.update(post.id, req.body);
res.json(updated);
});
Bei Ressourcen über verschiedene Tenants hinweg gibt es eine subtile Entscheidung bezüglich der Reihenfolge. Wenn ein Benutzer in Tenant A einen Beitrag in Tenant B anfordert, bestätigt die Rückgabe von 403, dass der Beitrag existiert, was Informationen über Tenants hinweg preisgibt. Viele Systeme geben in diesem Fall 404 zurück, sodass die Ressource nicht von einer nicht existierenden Ressource zu unterscheiden ist. Was auch immer Sie wählen, bleiben Sie konsistent und dokumentieren Sie es.
Das gleiche Muster gilt für verschachtelte Ressourcen. Bevor Sie auf /teams/:teamId/projects/:projectId zugreifen, verifizieren Sie, dass der Principal Zugriff auf das Team hat und dass das Projekt zu diesem Team gehört. Jede id im Pfad wird vom Angreifer kontrolliert und muss daher geprüft werden.
Die Berechtigungsmatrix
Die Berechtigungsmatrix ist eine Tabelle, in der Rollen als Zeilen und Berechtigungen als Spalten aufgeführt sind, gefüllt mit Zuweisungen. Sie ist das Artefakt, das ein Autorisierungssystem überprüfbar macht.
posts:read posts:create posts:update posts:delete billing:read
viewer x
editor x x x
admin x x x x x
billing x x
Speichern Sie diese in der Versionsverwaltung direkt neben dem Code und generieren Sie die Seed-Migrationen daraus, damit die Dokumentation und die Daten nicht auseinanderlaufen. Wenn jemand eine neue Rolle vorschlägt, ist die erste Frage, welche Spalten diese erhält – und die Antwort ist ein Diff dieser Tabelle, keine Suche durch die Handler.
Zwei Gewohnheiten machen die Matrix nützlich. Erstens: Benennen Sie Berechtigungen konsistent als resource:action, damit die Tabelle übersichtlich bleibt und die Strings vorhersehbar sind. Zweitens: Überprüfen Sie die Matrix jedes Mal, wenn eine Rolle geändert wird, da eine einzige zusätzliche Spalte in einer Migration leicht übersehen werden kann und weitaus mehr Rechte gewähren könnte als beabsichtigt.
Multi-tenant-Rollen
In einer Multi-tenant-Anwendung kann dieselbe Person in verschiedenen Organisationen unterschiedliche Rollen innehaben. Ein Agenturinhaber ist beispielsweise Admin im eigenen Tenant und Viewer im Tenant eines Kunden. Eine einzige globale role-Spalte kann dies nicht abbilden.
Die Lösung besteht darin, die Rollenzuweisungen auf den Tenant zu beschränken. Fügen Sie tenant_id zu user_roles hinzu und machen Sie es Teil des Primary Keys, sodass ein Benutzer pro Tenant unterschiedliche Rollen einnehmen kann. Wenn Sie das Permission Set für einen Request erstellen, erstellen Sie es für einen einzigen Tenant – und zwar für den, in dessen Kontext der Request ausgeführt wird.
SELECT DISTINCT p.action
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE ur.user_id = $1 AND ur.tenant_id = $2;
Der Tenant muss aus einer vertrauenswürdigen Quelle stammen: der Session, einer von Ihnen kontrollierten Subdomain oder dem Token. Akzeptieren Sie ihn niemals aus einem Request Body oder Query String, ohne zu verifizieren, dass der Principal tatsächlich zu diesem Tenant gehört. Sobald dies festgelegt ist, ist die Tenant-Isolierung die erste Regel in can() und greift, bevor irgendeine Berechtigung geprüft wird, sodass kein Grant jemals die Tenant-Grenze überschreiten kann.
Das Wechseln des Tenants ist eine Änderung der Privilegien. Wenn der aktive Tenant in der Session gespeichert ist, aktualisieren Sie diesen serverseitig und generieren Sie jedes gecachte Permission Set neu, damit die Grants des alten Tenants nicht in den neuen Kontext durchsickern können.
ABAC und Policy Engines
RBAC beantwortet die meisten Fragen, aber einige Regeln hängen von mehr als nur der Rolle und dem Besitz ab: die Tageszeit, die Sensibilität der Daten, die Abteilung des Benutzers oder der Risk Score der Anfrage. Dies sind attributbasierte Regeln, und der Versuch, diese als Rollen zu kodieren, führt zu einer kombinatorischen Explosion.
ABAC (Attribute-Based Access Control) wertet Policies basierend auf Attributen des Principals, der Ressource, der Aktion und der Umgebung aus. Eine Regel könnte so lauten: „Ein Benutzer darf ein Dokument lesen, wenn seine Abteilung mit der Abteilung des Dokuments übereinstimmt und die Klassifizierung nicht ‘geheim’ ist“. Das ist auf eine Weise ausdrückbar, testbar und prüfbar, wie es eine Rollenmatrix nicht ist.
Policy Engines machen dies in der Praxis anwendbar. Open Policy Agent wertet in Rego geschriebene Policies aus und kann als Sidecar oder Library abgefragt werden, sodass dieselben Regeln über verschiedene Services und Sprachen hinweg gelten. Casbin bietet einen leichteren Modell- und Adapter-Ansatz mit Unterstützung für RBAC, ABAC und Kombinationen daraus und ist im Anwendungscode weit verbreitet.
Setzen Sie eine Policy Engine erst dann ein, wenn die Regeln RBAC tatsächlich übersteigen, nicht vorher. Sie bringt eine neue Sprache, eine zusätzliche Deployment-Fläche und eine Lernkurve mit sich. Ein gut strukturierter can()-Helper mit klaren Regeln bewältigt überraschend viel, und Sie können ihn später jederzeit um eine Policy Engine erweitern, wenn eine spezifische Entscheidung mehr Kontext erfordert.
Autorisierung testen
Autorisierungsfehler sind Sicherheitslücken, daher sollten Tests die “Deny”-Fälle als erstklassige Bürger behandeln. Schreiben Sie für jede geschützte Aktion eine Testmatrix: ein anonymer Aufrufer, ein Principal ohne die entsprechende Berechtigung, ein Besitzer, ein Nicht-Besitzer mit der Berechtigung und ein Principal aus einem anderen Tenant.
describe("PATCH /posts/:id", () => {
it("rejects anonymous users", async () => {
await request(app).patch("/posts/1").send({ title: "x" }).expect(401);
});
it("rejects users without posts:update", async () => {
await request(app).patch("/posts/1").set("Cookie", viewerCookie).expect(403);
});
it("allows the owner", async () => {
await request(app).patch("/posts/1").set("Cookie", ownerCookie).expect(200);
});
it("rejects a non-owner editor", async () => {
await request(app).patch("/posts/1").set("Cookie", editorCookie).expect(403);
});
it("rejects a user from another tenant", async () => {
await request(app).patch("/posts/1").set("Cookie", otherTenantCookie).expect(404);
});
});
Testen Sie can() direkt als reine Funktion mit einer Tabelle aus Principals, Aktionen und Ressourcen. Damit wird die Logik erschöpfend und kostengünstig abgedeckt, während die Endpoint-Tests beweisen, dass die Prüfung tatsächlich implementiert ist. Ein häufiger Fehler ist ein korrekter Helper, dessen Aufruf im Handler vergessen wurde – das fängt nur ein Integrationstest ab.
Berechtigungen als Migrationen seeden
Berechtigungen und Rollenzuweisungen sind Teil des Vertrags Ihrer Anwendung. Daher gehören sie in Migrationen und nicht in ein Admin-Panel, das zwischen verschiedenen Umgebungen divergiert.
Schreiben Sie eine Seed-Migration, die Berechtigungen per Name upsert und anschließend die Zuweisungen jeder Rolle mit der Matrix abgleicht. Upserts halten die Migration idempotent, was wichtig ist, da sie gegen Datenbanken ausgeführt werden könnte, in denen bereits einige Zeilen existieren.
INSERT INTO permissions (action) VALUES
('posts:read'), ('posts:create'), ('posts:update'), ('posts:delete'),
('billing:read'), ('billing:manage')
ON CONFLICT (action) DO NOTHING;
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
JOIN permissions p ON p.action IN ('posts:read', 'posts:create', 'posts:update')
WHERE r.name = 'editor'
ON CONFLICT DO NOTHING;
Das Löschen einer Berechtigung ist riskanter als das Hinzufügen einer neuen. Prüfen Sie die Verwendung im Code und benennen Sie die Berechtigung schrittweise um oder führen Sie sie aus, falls sie noch irgendwo referenziert wird. Eine Migration, die posts:update löscht, während ein Handler diese noch prüft, führt dazu, dass jede Anfrage abgelehnt wird. Das ist zwar sicher, aber verwirrend, bis jemand den Seed-Diff liest.
Caching des Permission-Sets
Eine Berechtigungsprüfung sollte niemals direkt auf die Datenbank zugreifen. Erstellen Sie das effektive Set einmal pro Request, hängen Sie es an den Principal an und verwenden Sie es für jede weitere Prüfung innerhalb dieses Requests wieder.
Bei Systemen mit hohem Traffic sollten Sie das Set pro Benutzer für eine kurze Zeit cachen, wobei der Key aus Benutzer und Tenant besteht. Eine TTL von 30 bis 60 Sekunden reicht in der Regel aus, um die Abfrage aus dem Hot Path zu entfernen und gleichzeitig sicherzustellen, dass Rollenänderungen schnell sichtbar werden. Wenn eine Rolle geändert wird, invalidieren Sie den Cache explizit, anstatt auf das Ende der TTL zu warten, damit eine entzogene Berechtigung sofort aufhört zu funktionieren.
async function permissionsFor(userId: string, tenantId: string) {
const key = `perm:${tenantId}:${userId}`;
const cached = await redis.get(key);
if (cached) return new Set(JSON.parse(cached));
const rows = await db.query(permissionQuery, [userId, tenantId]);
const set = new Set(rows.map((r) => r.action));
await redis.set(key, JSON.stringify([...set]), "EX", 60);
return set;
}
Bei der TTL gibt es einen Security-Trade-off: Je länger der Cache ist, desto größer ist das Zeitfenster, in dem eine entzogene Rolle weiterhin funktioniert. Bevorzugen Sie eine explizite Invalidierung bei jeder Änderung der Rollenzuweisung und halten Sie die TTL kurz, um als Sicherheitsnetz für übersehene Invalidierungen zu dienen.
Best Practices
- Prüfen Sie im Anwendungscode Berechtigungen und nicht Rollennamen; behandeln Sie Rollen lediglich als Bündel für Menschen.
- Zentralisieren Sie die Entscheidung in einer einzigen pure
can(user, action, resource)Funktion. - Nutzen Sie “Deny by default” und stellen Sie sicher, dass das System im Fehlerfall geschlossen bleibt (“fail closed”), falls Rollen oder Berechtigungen nicht geladen werden können.
- Erzwingen Sie die Tenant-Isolation vor jeder Berechtigungsprüfung und beziehen Sie den Tenant aus einer vertrauenswürdigen Quelle.
- Laden Sie die Ressource, bevor Sie eine Aktion darauf autorisieren, um IDOR zu verhindern.
- Geben Sie 401 für nicht authentifizierte Anfragen und 403 für abgelehnte Anfragen zurück.
- Cachen Sie den effektiven Berechtigungssatz pro Request und invalidieren Sie diesen, wenn sich Rollen ändern.
- Verwalten Sie die Berechtigungsmatrix in der Versionsverwaltung und generieren Sie daraus die Seeds.
- Testen Sie die Negativfälle: anonyme Nutzer, falsche Berechtigung, Nicht-Besitzer, anderer Tenant.
- Loggen Sie “Allow”- und “Deny”-Entscheidungen mit ausreichend Kontext, um diese später nachvollziehen zu können.
Häufige Fehler
- Eine gültige Session oder ein Token als Beweis für die Autorisierung zu betrachten.
- Überall im Codebase Verzweigungen basierend auf
user.role === "admin"einzubauen. - Die Route zu prüfen, aber niemals die Ressource, was zu einer IDOR-Lücke führt.
- Einer Tenant-ID aus dem Request-Body oder dem Query-String zu vertrauen.
- Zu weitreichende
manage-Berechtigungen zu vergeben, um die Modellierung einer echten Regel zu vermeiden. - Tief verschachtelte Rollenhierarchien aufzubauen, die niemand mehr nachvollziehen kann.
- Berechtigungen dauerhaft zu cachen, sodass nach einer Rollenänderung veraltete Zugriffsrechte bestehen bleiben.
- Einen 403-Fehler zurückzugeben, obwohl ein 404 verhindern würde, dass die Existenz einer Ressource eines anderen Tenants preisgegeben wird.
- Ausgeblendete Buttons im UI als Ersatz für die Durchsetzung auf der Serverseite zu nutzen.
- Zu vergessen, jede ID in einem verschachtelten Route-Pfad zu prüfen.
Wie geht es weiter?
RBAC ist das Autorisierungsmodell, auf das Sie am häufigsten zurückgreifen werden, und es lässt sich nahtlos mit allem anderen kombinieren, was Sie bereits aufgebaut haben. Wenn Ihre Principals als Token übermittelt werden, zeigt der JWT-Guide, wo Claims wie Rollen ins Spiel kommen und warum Sie diese dennoch serverseitig verifizieren sollten. Der Principal selbst stammt aus der Session-Authentifizierung oder, im Falle von Maschinen, aus API keys. Da es bei der Autorisierung immer um ein Ziel geht, ist der REST-Guide der richtige Begleiter für die Modellierung von Ressourcen und deren IDs. Wenn Ihre Regeln beginnen, eher vom Kontext als von Rollen abzuhängen, kehren Sie zum ABAC-Abschnitt zurück und setzen Sie auf eine Policy Engine.