Autorisierung in .NET APIs: von Claims zu ressourcenbasierten Entscheidungen
Ist ein API-Endpoint für angemeldete Nutzer offen, kann es wirken, als sei die erste Sicherheitsschwelle bereits überwunden. Eine verifizierte Identität bedeutet jedoch nicht, dass eine Person jede Ressource sehen oder jede Operation ausführen darf. „Wer ist diese Person?“ und „Darf diese Person für diese Aktion auf diese Ressource zugreifen?“ sind verschiedene Fragen. Die erste betrifft Authentifizierung, die zweite Autorisierung.
Diese Trennung wird wichtiger, wenn .NET-Services wachsen. Anfangs wirken einige Rollenprüfungen im Controller ausreichend. Mit der Zeit kommen Tenant-Mitgliedschaft, Eigentümerschaft, Datensatzstatus, Abonnementpläne, Freigabeabläufe und Administratorausnahmen hinzu. Sind Regeln über Endpoints verteilt, wird Verhalten inkonsistent und ein neuer Endpoint kann denselben Schutz vergessen. Ziel ist nicht nur, Zugriff abzulehnen, sondern die Regel hinter einer Entscheidung explizit, wiederverwendbar und testbar zu machen.
Authentifizierungs- und Autorisierungsergebnis trennen
Eine Anfrage ohne gültigen Access Token oder mit ungültiger Identität erhält üblicherweise 401 Unauthorized: Der Client kann nicht beweisen, wer er ist. Fehlt einem authentifizierten Nutzer die erforderliche Berechtigung oder der Zugriff auf eine Ressource, ist 403 Forbidden meist das passendere Ergebnis.
Die Authentifizierungsschicht prüft Signatur, Laufzeit, Audience und Issuer eines Tokens und erzeugt dann einen vertrauenswürdigen ClaimsPrincipal. Die Autorisierungsschicht nutzt diese Identität und den Anwendungskontext, um eine Berechtigung zu bewerten. Nur weil ein Token eine Rolle enthält, direkt eine Operation zu erlauben, kann unzureichend sein, besonders wenn sich Mitgliedschaft oder Kontostatus ändern können.
Kurzlebige Tokens, Refresh-Flows und eine Strategie für Sitzungsbeendigung gehören ebenfalls zum Design. Muss eine Berechtigungsänderung sofort wirken, kann eine kritische Operation einen aktuellen Mitgliedschafts- oder Statuscheck statt nur der Token-Laufzeit brauchen.
Rollenprüfungen in Geschäftsregeln überführen
[Authorize(Roles = "Admin")] ist einfach und in manchen Fällen passend. Regeln wie „darf einen Bericht exportieren“, „darf den Entwurf einer anderen Person freigeben“ oder „darf die Abrechnungseinstellungen dieses Tenants ändern“ sind jedoch nicht nur Rollennamen. Sie enthalten Aktion, Ressource und Kontext.
Policy-basierte Autorisierung in .NET hilft, die Regel als benannte Anforderung zu zentralisieren. Eine Policy wie BillingSettingsWrite oder ReportExport kann relevante Claims, Rollen oder eigene Requirements verbinden. Ein Endpoint drückt dann nur seine Absicht aus:
[Authorize(Policy = "ReportExport")]
[HttpPost("reports/{id}/export")]
public async Task<IActionResult> Export(string id) { /* ... */ }
Dadurch lässt sich eine Policy konsistent anwenden, testen und dokumentieren. Ein Policy-Name sollte, wenn möglich, eine Produktfähigkeit ausdrücken. InvoiceApprove macht die Bedeutung einer Entscheidung im Review deutlicher sichtbar als CanDoThing2.
Entscheidung nach sicherem Laden gegen die Ressource treffen
Eine Person mit Berechtigung zum Report-Export darf möglicherweise nicht jeden Report exportieren. Ein Report kann einem anderen Tenant gehören, gesperrt sein oder nur für den Eigentümer sichtbar sein. Hier ist ressourcenbasierte Autorisierung nötig.
Ein solider Ablauf lädt die Ressource sicher im Tenant Context des Aufrufers und übergibt dann Nutzer, Ressource und Requirement an einen Authorization Handler. Der Handler bewertet Eigentümerschaft, Mitgliedschaft, Ressourcenstatus oder Geschäftsregel. Wie die äußere API „nicht gefunden“ und „nicht erlaubt“ unterscheidet, sollte ebenfalls eine konsistente Produkt- und Sicherheitsentscheidung sein.
Entscheidend ist, eine Ressource nicht zuerst mit einer uneingeschränkten Query zu laden und erst danach im Controller zu filtern. Die Datenschicht wendet Tenant- und Zugriffsgrenzen standardmäßig an. Autorisierung ist keine einzelne Prüfung nach einer schlechten Query; sie ist eine Verteidigung, die mit dem Query-Design beginnt.
Claims nicht als Echtzeitdatenbank behandeln
Claims eignen sich für relativ stabile Identitätsinformationen: Nutzeridentität, Status einer verifizierten E-Mail, Identity Provider oder allgemeine Rolle. Viele dynamische Berechtigungen, Ressourcenlisten oder große Tenant-Mitgliedschaftsdaten in einen Token zu füllen, kann ihn vergrößern und die Daten schnell veralten lassen.
Fragen, die sich mit einer Ressource ändern, etwa „Ist diese Person Editor dieses Projekts?“, sollten bei Bedarf aus Anwendungsdaten beantwortet werden. Es ist sicherer, diese Prüfung hinter einem Authorization Handler oder Application Service zu zentralisieren, als in jedem Endpoint ad hoc SQL zu schreiben. Ein kurzlebiger, sorgfältig geschlüsselter Cache kann helfen, seine Invalidierung bei Berechtigungsänderung muss jedoch von Anfang an geplant sein.
Jeden serverseitigen Einstiegspunkt schützen
Einen UI-Button zu verstecken, verbessert die Erfahrung, ist aber keine Sicherheitsgrenze. Ein Client kann eine Anfrage manuell erzeugen, ein älteres Mobile Release einen anderen Endpoint aufrufen oder ein Background Job eine Operation starten. HTTP-Endpoints, GraphQL Resolver, Message Consumer, Background Worker und Administrationsbefehle müssen jeweils ihren eigenen Autorisierungskontext prüfen.
Auch die Annahme, ein Service-Aufruf sei sicher, weil er aus einem internen Netzwerk kommt, reicht nicht. Identität des aufrufenden Services, Audience, minimaler Scope und die Frage, ob der Aufruf für einen Nutzer oder das System handelt, sollten explizit sein. Wird Nutzerkontext weitergegeben, muss das System prüfen, welche Claims vertrauenswürdig sind und wer sie signiert hat.
Für privilegierte Administration sollten starke Authentifizierung, enger Scope, Zeitgrenzen und Audit Records erwogen werden. Eine Administratorrolle darf kein Universalschlüssel sein, der alle Datengrenzen still übertritt.
Entscheidungen auditierbar, aber privat halten
Um einen Autorisierungsfehler zu diagnostizieren, kann geloggt werden, welche Policy für welchen Ressourcentyp mit welchem Ergebnis ausgewertet wurde. Access Tokens, vollständige Claim-Dumps, persönliche Inhalte oder sensible Ressourcendetails gehören nicht in Logs. Eine Request- oder Correlation-ID reicht dem Support oft, um einen Vorfall nachzuverfolgen.
Ein Audit Record beantwortet eine längerfristige Frage: Wer versuchte wann welche Aktion auf welcher Ressource und wie lautete die Entscheidung? Jede Leseanfrage detailliert zu speichern, kann teuer oder für die Privatsphäre unnötig sein. Hochriskante Schreibvorgänge, Exporte, Rollenänderungen und Administratoraktionen zu priorisieren, ist ausgewogener.
Autorisierung mit negativen Szenarien testen
Ein Test, in dem ein berechtigter Nutzer erfolgreich ist, beweist die Sicherheitsregel nicht. Für jede kritische Funktion sollten Personen ohne Identität, mit falscher Rolle, aus einem anderen Tenant, ohne Eigentümerschaft und mit abgelaufenem Kontext getestet werden. Auch die 401/403-Unterscheidung, Ressourcenentscheidung, Background Job und Administratorausnahme müssen abgedeckt sein.
Policy Handler lassen sich mit Unit Tests prüfen, für die Verbindung aus Endpoint und Datenzugriff eignen sich Integrationstests. Beim Hinzufügen eines neuen Endpoints sollte eine Autorisierungsanforderung auf die Code-Review-Checkliste gehören.
Gute Autorisierung bedeutet nicht, in jedem Controller mehr if-Anweisungen zu schreiben. Systeme, die Identität zuverlässig prüfen, Produktfähigkeiten über benannte Policies ausdrücken und Ressourcen- sowie Datengrenzen gemeinsam bewerten, vergeben weniger Privilegien und passen sich sicherer an sich ändernde Produktregeln an.