Lektion W4.1.1 hat Kladde.GraphLab zwei Berechtigungen anfordern lassen, User.Read und Calendars.Read, ohne zu erklären, warum gerade diese. Diese Lektion holt das nach. Berechtigungen sind bei Microsoft Graph keine Nebensache: Sie entscheiden, ob ein Aufruf mit 403 endet, und sie entscheiden, was passiert, wenn der Schlüssel einer App in falsche Hände gerät. Am Ende der Lektion hat die Kladde eine Rechteliste mit Begründung je Zeile, und Sie können ein Token lesen, eine Einwilligung einordnen und eine Fehlermeldung zu Berechtigungen auf ihre Ursache zurückführen.

Stand der Prüfung, bitte vorab lesen. Jede fachliche Aussage dieser Lektion stammt von Seiten auf Microsoft Learn, die beim Schreiben der Lektion gelesen wurden. Kein Befehl, kein Menüweg und kein Code dieser Lektion wurde gegen einen Microsoft-365-Mandanten ausgeführt, und der Code ist in der hier stehenden Fassung nicht gebaut worden. Wo eine Ausgabe stünde, beschreibt der Text, statt eine vorzutäuschen. Wo wir etwas nicht gelesen haben, steht „offen“. Menübezeichnungen im Entra Admin Center und in Exchange ändern sich; gilt Ihr Bildschirm anders als der Text, folgt der Text dem Stand der gelesenen Learn-Seiten.

Die Grundlagen von OAuth und OpenID Connect, die Anmeldung mit Entra ID und die erste App-Registrierung der Kladde kennen Sie aus Lektion 2.6.2 „OpenID Connect mit Microsoft Entra ID“. Wie die Kladde ohne Geheimnis an Azure kommt, steht in Lektion 3.3.1 „Keine Schlüssel: Managed Identity“, die Grundbegriffe von Rollen und Zuweisungen in Lektion 3.1.4 „Wer darf was: Entra ID und Azure RBAC“. Diese Lektion wiederholt das nicht. Sie baut darauf auf.

Zwei Arten des Zugriffs

Eine App kann auf zwei Arten an geschützte Daten kommen, und Learn trennt sie scharf:

  • Delegierter Zugriff: Eine Person hat sich angemeldet, die App arbeitet in ihrem Namen. Es braucht delegierte Berechtigungen, auch Bereiche (scopes) genannt.
  • Zugriff nur als App (app-only): Es ist niemand angemeldet, die App arbeitet mit ihrer eigenen Identität. Es braucht Anwendungsberechtigungen, auch App-Rollen genannt.

Learn nennt als Fälle für den zweiten Weg Automatisierung und Sicherung, Hintergrunddienste und Daemons: Er passt, wenn keine bestimmte Person angemeldet sein soll oder wenn die Daten nicht auf eine Person eingegrenzt werden können. Die Kladde hat beide Fälle: Eine Büroleiterin, die sich in der Kladde anmeldet und ihren Kalender sehen will, ist delegiert. Der Hintergrunddienst, der nachts Termine mit dem Büro-Kalender abgleicht, ist Anwendung.

Wichtiger als die Namen ist, wie weit der Zugriff reicht:

Delegiert Anwendung
Kontext im Namen einer angemeldeten Person ohne Person
Wirksam ist der Schnitt: was die App darf und was die Person selbst darf alle Daten, auf die die Berechtigung zielt
Beispiel Files.Read.All liest nur Dateien, die die Person selbst lesen darf liest jede Datei im Mandanten
Wer zustimmen darf Personen für ihre Daten, Administratoren für alle nur Administratoren (oder Besitzer der API)
Wie die Zustimmung zustande kommt statisch (Liste in der Registrierung) oder dynamisch (beim Anmelden nachgefragt) nur statisch (Liste in der Registrierung)
Andere Namen Scopes, OAuth2-Berechtigungsbereiche App-Rollen, App-only-Berechtigungen
Ergebnis der Zustimmung (bei Graph) oAuth2PermissionGrant appRoleAssignment

Bei delegiertem Zugriff müssen zwei Dinge stimmen: Die App muss den Bereich haben, und die Person muss selbst das Recht haben. Fehlt eines, scheitert der Aufruf. Das zweite Recht prüft die Quelle der Daten: bei Verzeichnisdaten Entra-Rollen, bei Postfächern und Kalendern Exchange Online. Learn nennt als Beispiele eine Rolle in Microsoft Entra RBAC für Verzeichnisdaten und Exchange Online RBAC für Mail und Kalender.

Bei Anwendungsrechten gibt es diese zweite Hürde nicht. Wer Mail.Read als Anwendung hat, liest jedes Postfach des Mandanten, wenn nichts anderes es einschränkt. Genau deshalb rät Learn aus Sicht der geringsten Rechte zum delegierten Modell, wo es genügt, und genau deshalb gibt es weiter unten in dieser Lektion einen Abschnitt, der Anwendungsrechte auf einzelne Postfächer zurückschneidet.

Dazu passt eine Zahlenangabe von Learn: Eine App kann bei Mandanten-Apps bis zu 400 Berechtigungen anfordern, bei Apps für persönliche Konten bis zu 30. Wer Hunderte braucht, hat ein anderes Problem.

Wie Berechtigungen heißen

Die Namen folgen dem Muster {Ressource}.{Operation}.{Einschränkung}: User.Read, Calendars.ReadWrite, Mail.Send, Files.Read.All, Sites.Selected. Die Teile bedeuten, was der Name sagt: .Read ist lesen, .ReadWrite lesen und schreiben, und der letzte Teil begrenzt: .All heißt „alle“ (alle Dateien, alle Postfächer), .Shared schließt Geteiltes ein, .Selected heißt „nur das, was Ihnen jemand ausdrücklich zugewiesen hat“. Die vollständige Liste, mit Beschreibung und der Angabe, ob eine Administratorzustimmung nötig ist, steht in Learns Permissions reference. Dort sehen Sie zum Beispiel Mail.Send als Anwendung mit „Send mail as any user without a signed-in user“ und „AdminConsentRequired: Yes“, als delegierte Berechtigung mit „No“.

Ein Satz, der in der Lernpraxis oft vergessen wird: Eine Berechtigung ist nicht gleich eine Rolle. Mail.Send ist eine Berechtigung der API Microsoft Graph. „Cloud Application Administrator“ ist eine Rolle im Verzeichnis, und „Application Mail.Send“ weiter unten ist eine Rolle in Exchange. Die Wörter ähneln sich, und die Systeme sind getrennt.

Einwilligung: wer darf zustimmen

Einwilligung (consent) ist der Vorgang, in dem Personen oder Administratoren einer App den Zugriff auf geschützte Daten erlauben. Learn unterscheidet:

  • Einwilligung durch die Person. Sie entsteht, wenn sich jemand bei einer App anmeldet. Erst werden die Anmeldedaten geprüft, dann, ob es schon eine Einwilligung für die verlangten Berechtigungen gibt. Fehlt sie, zeigt Entra ein Zustimmungsfenster mit der Liste der Berechtigungen und den Angaben zum Herausgeber. Stimmt die Person zu, wird das gespeichert, und bei den nächsten Anmeldungen kommt das Fenster in der Regel nicht wieder.
  • Einwilligung durch Administratoren. Manche Berechtigungen kann nur eine Administratorin geben: alle Anwendungsberechtigungen und viele delegierte Berechtigungen mit hohen Rechten. Eine Administratorin darf für sich selbst oder für die ganze Organisation zustimmen. Nach der Zustimmung für die Organisation fragt Entra die Personen nicht mehr (es sei denn, die App fordert später etwas an, dem die Administration noch nicht zugestimmt hat).

Ob Personen selbst zustimmen dürfen, entscheidet der Mandant. Ein Administrator mit der entsprechenden Rolle stellt es ein, und Learn nennt drei eingebaute Möglichkeiten:

  1. Einwilligung der Benutzer ist ausgeschaltet. Niemand darf neuen Berechtigungen zustimmen; vorhandene Zustimmungen bleiben. Nur Personen mit einer Verzeichnisrolle, die das erlaubt, dürfen für neue Apps zustimmen.
  2. Benutzer dürfen Apps von bestätigten Herausgebern (verified publisher) oder aus dem eigenen Mandanten zustimmen, aber nur für Berechtigungen, die der Mandant ausgewählt hat (als „geringe Auswirkung“ eingestuft).
  3. Benutzer dürfen allen Apps zustimmen, soweit die Berechtigungen keine Administratorzustimmung verlangen.

Das hat eine praktische Folge für die Kladde: Was bei Ihrem Test klappt, klappt beim Kunden vielleicht nicht, weil dessen Mandant die Einwilligung der Benutzer ausgeschaltet hat. Rechnen Sie bei jedem Kundenmandanten mit der Zustimmung durch die Administration, und schreiben Sie in Ihre Einführung, welche Berechtigungen die App braucht und warum. Eine App, die nach Rechten fragt, die sie nicht erklären kann, wird abgelehnt, und Learn schreibt dazu den Satz: Wenn Sie nicht sicher sind, wer die App kontrolliert und warum sie diese Berechtigungen anfordert, geben Sie die Zustimmung nicht.

Wer darf die Administratorzustimmung geben

Learn nennt für die Zustimmung im Namen der ganzen Organisation drei Gruppen:

  • Privileged Role Administrator: für Apps, die beliebige Berechtigungen für beliebige APIs anfordern.
  • Cloud Application Administrator, Application Administrator (und weitere): für beliebige Berechtigungen beliebiger APIs, außer den App-Rollen von Microsoft Graph (Anwendungsberechtigungen).
  • Eine benutzerdefinierte Verzeichnisrolle, die das Recht enthält, Anwendungen Berechtigungen zu erteilen.

Der Satz mit der Ausnahme gehört in Ihre Einführung: Anwendungsberechtigungen für Graph gibt nur ein Privileged Role Administrator (oder eine Rolle, die dem entspricht). Wer als Cloud Application Administrator eine Anwendungsberechtigung für Graph zustimmen soll, kann es laut Learn nicht.

Zwei Wege führen im Admin Center zum Ziel, beide von Learn beschrieben:

  • In den Unternehmensanwendungen: Entra ID, Enterprise apps, All applications, die App wählen, unter Security die Permissions öffnen, die verlangten Berechtigungen sorgfältig prüfen und Grant admin consent wählen. Das geht, wenn die App schon im Mandanten angelegt ist, etwa weil sich schon eine Person angemeldet hat.
  • In den App-Registrierungen: Entra ID, App registrations, All applications, die App wählen, unter Manage die API permissions öffnen, prüfen und Grant admin consent wählen. Das ist der Weg für Apps, die Ihre Organisation selbst registriert hat.

Learn nennt außerdem die Adresse für die Zustimmung der Organisation, mit der Sie einer Administratorin den Weg schicken können, ohne ein Wort über Geheimnisse zu verlieren:

https://login.microsoftonline.com/{organization}/adminconsent?client_id={client-id}

{client-id} ist die Anwendungs-ID der App, {organization} die Mandanten-ID oder eine bestätigte Domäne des Mandanten (der Wert organizations lässt die Zustimmung im Heimatmandanten der anmeldenden Person geschehen). Die Adresse enthält keine Geheimnisse, nur die ID der App. Schicken Sie einer Administration diese Adresse und eine Liste der Berechtigungen mit Begründung, nie ein Client-Geheimnis, in keinem Chat und in keiner Mail. Was die Seite „Admin consent on the Microsoft identity platform“ zu weiteren Parametern sagt, haben wir nicht gelesen (offen).

Drei Warnungen von Learn, die Sie in die Einführung übernehmen können:

  1. Die Zustimmung für die Organisation ist ein sensibler Vorgang: Sie kann dem Herausgeber der App Zugriff auf große Teile der Daten geben, etwa auf alle Postfächer oder alle Websites.
  2. Sie erlaubt standardmäßig allen Personen im Mandanten, die App zu nutzen. Wer das einschränken will, stellt die App auf Benutzerzuweisung erforderlich und weist Personen oder Gruppen zu.
  3. Sie kann bereits erteilte mandantenweite Berechtigungen der App widerrufen. Berechtigungen, die Personen für sich selbst gegeben haben, bleiben.

Learn zeigt auch, wie man die Zustimmung per Skript erteilt, mit Microsoft Graph PowerShell oder mit der Graph API selbst (POST /oauth2PermissionGrants mit consentType: AllPrincipals für delegierte Berechtigungen und POST /servicePrincipals/{id}/appRoleAssignedTo für Anwendungsberechtigungen). Dazu die ausdrückliche Warnung: Was per Skript erteilt wird, unterliegt keiner Prüfung oder Bestätigung und gilt sofort. Ein solches Skript gehört in ein Repository, mit einem Probelauf (-WhatIf), und Lektion W4.3.2 kommt darauf zurück. Hier gilt: Erst die Berechtigungen lesen, die das Skript erteilt, dann laufen lassen.

Anfragen an die Administration

Dürfen Personen nicht selbst zustimmen, bietet Entra den Genehmigungsworkflow (admin consent workflow) an: Die Person sieht „Approval required“, schreibt eine Begründung und fordert Zustimmung an, die benannten Prüfenden bekommen eine Benachrichtigung, und die Person erfährt das Ergebnis. Mehrere Anfragen derselben Person gehen nur einmal an die Administration. Ob der Mandant Ihres Kunden diesen Workflow eingeschaltet hat, ist seine Sache. Ihr Teil ist die Begründung, die Sie der Person mitgeben.

Schrittweise Einwilligung

Bei delegierten Berechtigungen kann eine App zunächst wenig anfordern und später mehr: Learn nennt es inkrementelle und dynamische Einwilligung. Das ist die richtige Wahl für optionale Funktionen. Die Kladde fragt bei der Anmeldung nur nach dem Nötigen. Erst wer die Kalenderfunktion einschaltet, sieht das Zustimmungsfenster für Calendars.Read. In der ASP.NET-Core-Anbindung von Microsoft.Identity.Web geben Sie dafür bei einem einzelnen Aufruf die zusätzlichen Bereiche an; Learn zeigt es so:

C#
var messages = await _graphClient.Me.Messages
    .GetAsync(r => r.Options.WithScopes("Mail.Read"));

Das setzt das Paket Microsoft.Identity.Web.GraphServiceClient voraus (services.AddMicrosoftGraph()), das Sie aus Lektion W4.1.1 als den Weg für ASP.NET Core kennen. Anwendungsberechtigungen kennen das nicht: Sie sind immer statisch, also in der Registrierung eingetragen. Wer als Anwendung arbeitet, fordert den Bereich https://graph.microsoft.com/.default an und bekommt damit, was in der Registrierung steht und was zugestimmt wurde. Learn sagt dazu ausdrücklich: Der Ablauf mit Client-Anmeldedaten verlangt /.default, die Berechtigungen müssen vorher in der Registrierung stehen, und eine Administration muss vorher zugestimmt haben.

Eine kleine Warnung zu .default: Es heißt „alles, was die App hat“. Es ist deshalb bequem, aber es zeigt dem Code nicht, was er braucht. Die Rechteliste unten führt das Wissen, nicht der Code.

Die App-Registrierung

Damit eine App Entra nutzen darf, muss sie registriert sein. Die Registrierung stellt eine Vertrauensbeziehung zwischen der App und der Microsoft-Identitätsplattform her, und zwar einseitig: Die App vertraut der Plattform, nicht umgekehrt. Ein einmal angelegtes Anwendungsobjekt lässt sich nicht in einen anderen Mandanten verschieben. Das Anlegen verlangt mindestens die Rolle Cloud Application Administrator.

Beim Anlegen entscheiden Sie, wer die App nutzen darf („Supported account types“): nur Konten im eigenen Verzeichnis (eine Fachanwendung für die eigene Firma, „einmandantenfähig“), Konten in beliebigen Verzeichnissen (Mehrmandanten-App, etwa für Software als Dienst) oder zusätzlich persönliche Microsoft-Konten. Die Kladde bei der Nordlicht Reisen GmbH ist einmandantenfähig. Danach zeigt die Übersicht zwei Werte: die Anwendungs-ID (Client-ID), die die App in allen Mandanten kennzeichnet, und die Objekt-ID, die sie in Ihrem Mandanten kennzeichnet. Beide sind keine Geheimnisse; verwechseln Sie sie nicht, denn Exchange (siehe unten) will beide an verschiedenen Stellen.

Wie sich die App ausweist

Eine App, die ohne Person arbeitet (ein vertraulicher Client), braucht eine Anmeldeinformation. Learn nennt drei:

Anmeldeinformation Learn sagt
Schlüsselpaar mit öffentlichem Teil (certificate) die empfohlene Art, sicherer als ein Geheimnis; hochgeladen wird die Datei mit dem öffentlichen Teil (.cer, .pem oder .crt)
Client-Geheimnis weniger sicher; für die lokale Entwicklung gebräuchlich, für die Produktion nicht gedacht; Laufzeit höchstens zwei Jahre (24 Monate), Microsoft empfiehlt unter zwölf Monaten; der Wert wird nur einmal angezeigt
Föderierte Anmeldeinformation ein Arbeitslast-Beweis, etwa von GitHub Actions oder einem Kubernetes-Dienstkonto, ganz ohne Geheimnis

Für die Kladde heißt das, in dieser Reihenfolge: K2 nimmt das Client-Geheimnis in den User Secrets (Lektion 2.6.2, Lektion 2.8.5), weil es in der Entwicklung am einfachsten ist. Ab K3 trägt eine verwaltete Identität das Token, ohne dass irgendwo ein Geheimnis liegt (Lektion 3.3.1). Dass einer verwalteten Identität Graph-Anwendungsberechtigungen zugewiesen werden, ist Stoff der Lektion W4.3.2. Der Schlüssel, das Geheimnis, gehört nie in das Repository, nie in die Projektdatei und nie in eine Chat-Nachricht.

Auf die Frage, ob der Hintergrunddienst der Kladde seine Anmeldung teilen soll, gibt Learn keine Antwort. Wir geben eine als Entwurfsempfehlung (nicht aus Learn): Eine Registrierung je Zweck.

Registrierung Zweck Art Anmeldeinformation
Kladde (Entwicklung) aus Lektion 2.6.2 Anmeldung der Personen in der Kladde delegiert (nur Anmeldung) Geheimnis in den User Secrets, später keines
Kladde Büro (Entwicklung), neu Hintergrunddienst: Mail senden, Kalender abgleichen Anwendung Geheimnis in den User Secrets, ab K3 verwaltete Identität
kladde-graphlab-test, neu, nur im Testmandanten Werkzeug Kladde.GraphLab delegiert keine (öffentlicher Client, Gerätecode)

Die Begründung: Eine Anwendungsberechtigung auf „alle Postfächer“ gehört nicht in die Registrierung, mit der sich jede Person anmeldet; ein Geheimnis, das den Büro-Dienst öffnet, gehört nicht in dasselbe Objekt wie die Anmeldung; und Exchange (siehe unten) vergibt Rechte an eine Identität. Wer zwei Zwecke vermischt, kann sie nicht getrennt einschränken und nicht getrennt widerrufen.

Die kleinste Berechtigung wählen

Jede Referenzseite eines Graph-Aufrufs hat einen Abschnitt Permissions mit einer Tabelle, getrennt nach delegiert (Arbeits- oder Schulkonto, persönliches Konto) und Anwendung, mit zwei Spalten: Least privileged permissions und Higher privileged permissions. Learn sagt dazu: Wählen Sie die als kleinste markierte Berechtigung; nehmen Sie eine höhere nur, wenn Ihre App sie wirklich braucht. Auszüge aus den Seiten, die wir gelesen haben (jeweils v1.0):

Aufruf Delegiert (Arbeits- oder Schulkonto) Anwendung
GET /me, GET /users/{id} (user-get) kleinste User.Read; höher unter anderem User.Read.All kleinste User.Read.All
calendarView (user-list-calendarview) kleinste Calendars.ReadBasic; höher Calendars.Read, Calendars.ReadWrite kleinste Calendars.ReadBasic; höher Calendars.Read, Calendars.ReadWrite
sendMail (user-sendmail) Mail.Send; höher: nicht vorhanden Mail.Send; höher: nicht vorhanden
Termin anlegen (calendar-post-events) Calendars.ReadWrite Calendars.ReadWrite

Zwei Beobachtungen daraus:

  1. Lesen und Schreiben sind verschiedene Berechtigungen. Wer Termine anlegt, braucht Calendars.ReadWrite. Learn führt ein Beispiel an, das genau die Kladde trifft: Eine App, die Kalenderdaten aus Outlook liest und Termine schreibt, braucht Calendars.ReadWrite, auch wenn das Holen der Änderungen (Delta) allein nur Calendars.Read verlangt. Die App fordert also eine Berechtigung an, die zum höchsten Bedarf passt.
  2. „Kleinste“ gilt je Aufruf, nicht je App. Ihre App fordert die Vereinigung der kleinsten Berechtigungen aller ihrer Aufrufe an, nicht mehr.

Eine Falle für calendarView: Calendars.ReadBasic ist laut Referenz die kleinste. Offen ist, welche Eigenschaften eines Termins damit ankommen: Den Text der Berechtigung haben wir nicht gelesen. Wer den Betreff braucht und ihn mit der kleinsten Berechtigung nicht bekommt, nimmt die nächsthöhere. Das klären Sie im Testmandanten; die Rechteliste unten schreibt es als Prüfauftrag auf.

Eine Berechtigung, die sich auf eine einzige Stelle beschränkt: .Selected

Für Dateien gibt es eine eigene Familie: Weil das Berechtigungsmodell von SharePoint und OneDrive nicht in das Raster der Bereiche passt, gibt es Selected-Bereiche: Sites.Selected für eine bestimmte Websitesammlung, außerdem Lists.SelectedOperations.Selected, ListItems.SelectedOperations.Selected und Files.SelectedOperations.Selected für eine Liste, Listenelemente oder Dateien und Ordner. Alle unterstützen delegiert und als Anwendung. Wichtig: Die Zustimmung zu Sites.Selected allein gibt keinen Zugriff auf irgendeine Website. Danach muss eine Administration die App für jede Website ausdrücklich zulassen (nach den gelesenen Seiten über POST /sites/{siteId}/permissions mit einer Rolle wie read, write, manage oder fullcontrol und der Client-ID der App). Das ist der Weg, einer App den Zugriff auf eine Website statt auf alle zu geben. Lektion W4.3.1 macht das mit der Kladde. Für Teams gibt es einen Weg, die Berechtigung an einzelne Teams, Chats und Nachrichten zu binden, die ressourcenspezifische Einwilligung (resource-specific consent, RSC). Was Learn für Teams-Kanalnachrichten im Einzelnen sagt, lesen wir erst in Lektion W4.3.1 (offen).

Die Rechteliste der Kladde

Aus alledem entsteht die Liste, die jeder Kundenmandant bekommt. Eine Zeile je Aufgabe, mit Art, Berechtigung und Begründung. Der Stand nach dem Kurs, soweit er gelesen ist:

Aufgabe Art Berechtigung Begründung Eingrenzung
Anmeldung der Personen delegiert nur Anmeldung (K2) die Kladde braucht keine Graph-Daten der Person keine nötig
Bestätigungsmails senden (K2 2.8.5) Anwendung Mail.Send der Dienst sendet ohne Person; für sendMail gibt es nichts Kleineres Exchange RBAC for Applications, auf ein Postfach (buero@…)
Termine des Büros lesen und schreiben (W4.2.2, W4.2.3) Anwendung Calendars.ReadWrite wir schreiben Termine; Lesen und Schreiben brauchen dieselbe höhere Berechtigung (Beispiel von Learn) Exchange RBAC for Applications, auf die Büro-Postfächer
Anzeige „meine Termine heute“ im Werkzeug delegiert User.Read, Calendars.Read (Prüfauftrag: reicht Calendars.ReadBasic?) die Person sieht nur, was sie selbst sieht durch das Delegiertenmodell
Dateien ablegen (W4.3.1) Anwendung Sites.Selected genau eine Website Zuweisung auf die Website per sites/{id}/permissions
Meldung im Teams-Kanal (W4.3.1) offen offen (nicht gelesen) wird in W4.3.1 gelesen offen

Jede Zeile enthält einen Satz „warum nicht weniger“. Wer das nicht hinschreiben kann, hat die Berechtigung noch nicht verstanden. Und die Spalte Eingrenzung ist keine Dekoration: Bei Anwendungsberechtigungen auf Postfächer ist sie die Grenze, wie wir gleich sehen.

Ein Token lesen

Ob eine App delegiert oder als Anwendung unterwegs ist, sehen Sie im Token. Learn beschreibt zwei Claims:

  • scp: eine durch Leerzeichen getrennte Liste der delegierten Bereiche, für die die App eine Zustimmung hat. Er steht nur in Benutzertoken.
  • roles: eine Liste von Berechtigungen. Bei Anwendungstoken sind es die App-Rollen der App. Bei Benutzertoken enthält roles dagegen die Rollen der Person in der Ziel-API: Der Claim kann also auch in einem delegierten Token stehen, bedeutet dort aber etwas anderes.

Daraus folgt eine Regel zum Lesen: Erst scp prüfen. Steht scp im Token, ist es ein delegiertes Token, und roles nennt die Rollen der Person. Fehlt scp, ist es ein Anwendungstoken, und roles sind die Berechtigungen. Learns Beispiele dafür:

JSON
"scp": "Directory.Read.All User.Read",
JSON
"roles": [
    "User.Read.All"
],

Ergänzend: Der optionale Claim idtyp hat bei Anwendungstoken den Wert app und ist der einfachste Weg, ein Token ohne Person von einem mit Person zu unterscheiden (Learn nennt ihn im Zusammenhang mit der Kombination aus scp und roles). Und: Anwendungstoken entstehen nur im Ablauf mit Client-Anmeldedaten (client credentials); delegierte Token entstehen aus Abläufen mit einer Person (Autorisierungscode, Gerätecode, On-Behalf-Of und einigen weiteren). Wenn Sie Anwendungsrechte vergeben haben und mit einem Token vom Autorisierungscode-Ablauf arbeiten, sehen Sie nichts davon, und der Aufruf endet mit 403. Learn nennt genau diesen Fall unter den Ursachen: Die Art der Berechtigung und die Art des Tokens müssen zusammenpassen.

Lesen darf man Token zum Verstehen. Entscheiden darf man nach ihnen nicht: Learn lässt die API die Claims prüfen (die aufgerufene Seite validiert Signatur, Zielgruppe und Bereiche), nicht den Aufrufer. Und ein Token ist ein Geheimnis. Wer es in ein Webwerkzeug zum Entschlüsseln einfügt, gibt es aus der Hand; Learn nennt jwt.ms als Werkzeug dafür, wir halten es für Token aus Testmandanten für vertretbar und für alle anderen für unnötig, weil das Lesen der Claims in .NET wenige Zeilen sind. Der Ausschnitt unten gibt nie das Token aus, nur seine Berechtigungen:

C#
// src/Kladde.GraphLab/TokenPermissions.cs
using System.Buffers.Text;
using System.Text.Json;

namespace Kladde.GraphLab;

public enum TokenKind
{
    Unknown,
    Delegated,
    Application,
}

/// <summary>
/// What the claims of an access token say, for a human. It reads the payload and checks nothing: no signature, no audience, no expiry.
/// Nothing may be authorized with it; the API that receives the token does that.
/// </summary>
public sealed record TokenPermissions(TokenKind Kind, IReadOnlyList<string> Permissions)
{
    private static readonly TokenPermissions Nothing = new(TokenKind.Unknown, []);

    public static TokenPermissions Read(string jwt)
    {
        string[] parts = jwt.Split('.');
        if (parts.Length != 3)
        {
            return Nothing;
        }

        try
        {
            using JsonDocument payload = JsonDocument.Parse(Base64Url.DecodeFromChars(parts[1]));
            JsonElement root = payload.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
            {
                return Nothing;
            }

            // scp first: a user token may carry roles too, but those are the person's roles in the API.
            if (root.TryGetProperty("scp", out JsonElement scope) && scope.ValueKind == JsonValueKind.String)
            {
                string[] scopes = (scope.GetString() ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries);
                return new TokenPermissions(TokenKind.Delegated, scopes);
            }

            bool isAppToken = root.TryGetProperty("idtyp", out JsonElement type)
                && type.ValueKind == JsonValueKind.String
                && type.GetString() == "app";
            if (root.TryGetProperty("roles", out JsonElement roles) && roles.ValueKind == JsonValueKind.Array)
            {
                string[] names = roles.EnumerateArray().Select(role => role.GetString() ?? "").Where(name => name.Length > 0).ToArray();
                return new TokenPermissions(TokenKind.Application, names);
            }

            return isAppToken ? new TokenPermissions(TokenKind.Application, []) : Nothing;
        }
        catch (Exception exception) when (exception is FormatException or ArgumentException or JsonException)
        {
            return Nothing;
        }
    }
}

Base64Url ist ein Typ aus .NET, nicht aus Graph; das Format der Token (drei Teile, durch Punkte getrennt, der mittlere Base64-URL-kodiert) ist das von JSON Web Tokens, das Learn in „Tokens and claims overview“ beschreibt. Ein Anwendungstoken ohne roles, aber mit idtyp: app, ist ein Sonderfall, den der Code ausdrücklich als „Anwendung ohne Berechtigung“ zeigt: Learn spricht von „role-less app-only access tokens“, mit denen eine API nichts anfangen soll.

Anwendungsrechte auf Postfächer eingrenzen

Zurück zum Satz von weiter oben: Anwendungsberechtigungen auf Mail, Kalender, Kontakte und Postfacheinstellungen gelten für alle Postfächer der Organisation. Mail.Send als Anwendung heißt: als jedes Postfach senden. Das ist für den Dienst der Kladde, der von buero@… senden soll, viel zu viel. Learn bietet zwei Wege, das zu beschränken, und einer davon ist veraltet.

RBAC for Applications (der aktuelle Weg)

Role Based Access Control for Applications in Exchange Online schneidet die Berechtigung nicht in Entra ID, sondern in Exchange zu und koppelt sie an einen Geltungsbereich: Welche Postfächer darf die App erreichen? Learn fasst den Unterschied so zusammen: Berechtigungen für die ganze Organisation vergeben Sie in Entra ID, auf Ressourcen eingeschränkte Berechtigungen mit RBAC for Applications. Es unterstützt bis zu 10.000 Apps je Organisation und ersetzt die Application Access Policies.

Der Aufbau, in Learns Worten: fünf Schritte.

  1. Einen Geltungsbereich anlegen (optional): eine Management Scope, ein Filterausdruck über Eigenschaften der Postfächer, oder eine Administrative Unit aus Entra ID.
  2. Einen Zeiger auf den Dienstprinzipal in Exchange anlegen. Der Dienstprinzipal selbst wird in Entra verwaltet; Exchange legt nur einen Verweis an und merkt Löschungen in Entra selbst.
  3. Die passende Anwendungsrolle wählen, eine besondere Verwaltungsrolle in Exchange, die nur einer Anwendung zuweisbar ist. Beispiele aus der Liste von Learn: Application Mail.Send, Application Mail.Read, Application Calendars.Read, Application Calendars.ReadWrite, Application Mail Full Access.
  4. Die Rollenzuweisung anlegen: Dienstprinzipal, Rolle und Geltungsbereich zusammen.
  5. Mit einem Testbefehl prüfen.

Die Befehle, aus Learn, mit unseren Platzhaltern:

PowerShell
New-ServicePrincipal -AppId <client-id> -ObjectId <service-principal-object-id> -DisplayName "Kladde Büro"

New-ManagementScope -Name "Kladde Buero" -RecipientRestrictionFilter "Alias -eq 'buero'"

New-ManagementRoleAssignment -Name "Kladde Mail.Send Buero" -Role "Application Mail.Send" -App <service-principal-object-id> -CustomResourceScope "Kladde Buero"

Test-ServicePrincipalAuthorization -Identity <service-principal-object-id> -Resource buero

Hinweise dazu, jeder aus Learn:

  • Die IDs nicht verwechseln. New-ServicePrincipal verlangt die ID aus den Unternehmensanwendungen, nicht die aus den App-Registrierungen: „Don't use the IDs from the App Registrations page, as it shows different values.“ Die Objekt-ID, die Sie brauchen, ist die des Dienstprinzipals; die Anwendungs-ID ist die Client-ID.
  • Voraussetzung: Mitglied der Rollengruppe Organization Management in Exchange und in Entra die Rolle Exchange Administrator.
  • Der Test: Test-ServicePrincipalAuthorization simuliert, was die Rollenzuweisungen erlauben, ohne das Zwischenspeichern, und nennt für jedes angefragte Postfach InScope mit True oder False. Ohne -Resource steht dort „Not Run“. Das Ergebnis berücksichtigt nicht, was Entra ID der App separat gegeben hat.
  • Zeit: Änderungen wirken verzögert: Der Zwischenspeicher einer App ohne Aufrufe wird nach 30 Minuten geleert, der einer aktiven App bleibt bis zu 2 Stunden. Der Testbefehl ist davon ausgenommen. Wer nach einer Änderung sofort mit der echten App testet, ist bis zu zwei Stunden zu früh.
  • Grenzen: Anwendungsrollen sind nur Dienstprinzipalen zuweisbar; ausschließliche Geltungsbereiche (exclusive scopes) schränken den Zugriff der App nicht ein; der Autodiscover-Dienst geht mit Anwendungsrollen derzeit nicht. Unterstützte Protokolle sind Microsoft Graph und EWS.

Die wichtigste Regel steht im Abschnitt „FAQ“ von Learn: Die Rechte aus Entra ID und aus Exchange werden addiert. Wer Mail.Send in Entra ID zugestimmt hat und zusätzlich Application Mail.Send auf ein Postfach in Exchange zuweist, hat keine Eingrenzung: Die Vereinigung aus „alle Postfächer“ und „ein Postfach“ ist „alle Postfächer“. Für die Kladde heißt das: Für den Büro-Dienst wird Mail.Send nicht in Entra ID zugestimmt, sondern nur in Exchange zugewiesen. In Entra ID bleibt die Liste der Anwendungsberechtigungen für Kladde Büro (Entwicklung) für diese Rechte leer. Learn nennt die beiden Quellen unabhängig voneinander; offen ist, was ein Token einer solchen App in roles enthält, das haben wir nicht gelesen. Wundern Sie sich nicht, wenn Sie in Entra ID nichts finden: Eine gemeinsame Ansicht der Rechte aus beiden Quellen im Entra-Admin-Center ist bei Learn als „upcoming“ markiert.

Application Access Policies (der alte Weg)

Eine Application Access Policy schränkt nur die Berechtigungen ein, die in Entra ID zugestimmt wurden, und zwar auf die Mitglieder einer Gruppe. Learn markiert sie als veraltet (legacy) und sagt, RBAC for Applications ersetzt sie. Für neue Einrichtungen nutzen Sie sie nicht. Wer sie in einem Kundenmandanten vorfindet, kann sie migrieren: Management Scope mit einem Filter auf die Gruppe (MemberOfGroup, Unterschied: nur direkte Mitglieder zählen, verschachtelte nicht), Dienstprinzipal-Zeiger, Rollenzuweisung mit Geltungsbereich, dann die Zustimmung in Entra ID entfernen und dann die Richtlinie. Beide gleichzeitig zu betreiben, nennt Learn einen Randfall, „den wir nicht erwarten“, und zeigt zwei Beispiele, in denen die Mischung zu Ablehnung oder zu mehr Zugriff als gedacht führt.

Das Skript dazu

Der Dienst der Kladde bekommt seine Eingrenzung einmal, in jedem Mandanten, vor dem ersten Aufruf. Jedes Kommando, das einen Mandanten verändert, gehört in ein Skript, das im Repository liegt, einen Probelauf (-WhatIf) kennt und den Mandanten erst nach der Bestätigung anfasst:

PowerShell
# scripts/runbooks/graph/kladde-mailbox-scope.ps1
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory)][string]$ServicePrincipalAppId,       # <client-id> of the app registration
    [Parameter(Mandatory)][string]$ServicePrincipalObjectId,    # object ID of the ENTERPRISE application, not of the registration
    [Parameter(Mandatory)][string]$MailboxAlias,
    [ValidateSet('Application Mail.Send', 'Application Calendars.Read', 'Application Calendars.ReadWrite')]
    [string]$Role = 'Application Mail.Send',
    [switch]$Remove
)

$ErrorActionPreference = 'Stop'
$scopeName = "Kladde $MailboxAlias"
$assignmentName = "Kladde $Role $MailboxAlias"

if ($PSCmdlet.ShouldProcess('Exchange Online', 'Connect-ExchangeOnline')) {
    Connect-ExchangeOnline -ShowBanner:$false
}

if ($Remove) {
    if ($PSCmdlet.ShouldProcess($assignmentName, 'Remove-ManagementRoleAssignment')) {
        Remove-ManagementRoleAssignment -Identity $assignmentName -Confirm:$false
    }
    if ($PSCmdlet.ShouldProcess($scopeName, 'Remove-ManagementScope')) {
        Remove-ManagementScope -Identity $scopeName -Confirm:$false
    }
    return
}

if ($PSCmdlet.ShouldProcess($ServicePrincipalObjectId, 'New-ServicePrincipal')) {
    New-ServicePrincipal -AppId $ServicePrincipalAppId -ObjectId $ServicePrincipalObjectId -DisplayName 'Kladde Buero'
}
if ($PSCmdlet.ShouldProcess($scopeName, "New-ManagementScope (Alias -eq '$MailboxAlias')")) {
    New-ManagementScope -Name $scopeName -RecipientRestrictionFilter "Alias -eq '$MailboxAlias'"
}
if ($PSCmdlet.ShouldProcess($assignmentName, 'New-ManagementRoleAssignment')) {
    New-ManagementRoleAssignment -Name $assignmentName -Role $Role -App $ServicePrincipalObjectId -CustomResourceScope $scopeName
}
if ($PSCmdlet.ShouldProcess($MailboxAlias, 'Test-ServicePrincipalAuthorization')) {
    Test-ServicePrincipalAuthorization -Identity $ServicePrincipalObjectId -Resource $MailboxAlias | Format-Table
}

-WhatIf ruft nichts davon auf, auch nicht die Anmeldung: Es schreibt nur, was es täte. Das ist der Teil, den Sie ohne Mandanten laufen lassen können. Die Aufräumfassung mit -Remove entfernt Zuweisung und Geltungsbereich; den Zeiger auf den Dienstprinzipal löscht Remove-ServicePrincipal (von Learn genannt, hier nicht eingebaut, weil ein Dienstprinzipal in Exchange auch von anderen Zuweisungen gebraucht werden kann). Für ein Mehrmandanten-Szenario fehlt dem Skript noch einiges; es ist ein Anfang, und Lektion W4.3.2 baut es aus.

Verwaltung der Einwilligungen

Eine Einwilligung ist nicht mit der Zustimmung vorbei. Sie ist ein Zustand, den jemand pflegen muss:

  • Nachsehen: In den Unternehmensanwendungen unter Permissions sieht eine Administration, was einer App erlaubt ist. Das ist die Stelle, an der Sie einem Kunden zeigen, dass die Liste Ihrer Rechteliste entspricht.
  • Änderungen: Ändert die App ihre Berechtigungen, sieht die Person das Zustimmungsfenster neu. Learn nennt weitere Gründe für unerwartete Fenster: eine App mit Pflicht zur Benutzerzuweisung (dann zählt nur die Zustimmung für die Organisation), ein anderer Benutzer als der, der zugestimmt hat, eine Zustimmung, die nur für die zustimmende Person galt, und eine widerrufene Zustimmung.
  • Zugriff begrenzen geht unabhängig von der Zustimmung: App auf Benutzerzuweisung erforderlich stellen und Personen oder Gruppen zuweisen.
  • Skripte (Graph PowerShell oder Graph API) ändern Zustimmungen sofort und ohne Rückfrage. Lesen Sie zuerst die Berechtigungen, die das Skript erteilt.
  • Wie Zustimmungen widerrufen werden, haben wir nicht gelesen (offen). Learn führt es auf eigenen Seiten.

Und für die Kommunikation mit einer Administration gilt, wie oben: Sie bekommt eine Liste mit Begründung und die Adresse für die Zustimmung, sonst nichts. Einen Wert, den Sie aus der Registrierung kopieren, schicken Sie einem Fremden nur, wenn er in Ihrer Rechteliste steht: Anwendungs-ID und Mandanten-ID. Das Client-Geheimnis nie.

Typische Fehler

Learn führt die Gründe, die ein 403 haben kann, auf einer Seite zur Behebung von Autorisierungsfehlern auf. Die Auswahl, die für die Kladde zählt:

Meldung Bedeutung Was Sie prüfen
401 Unauthorized Das Token fehlt, ist ungültig oder abgelaufen; auch ein delegiertes Token eines persönlichen Kontos für eine API, die nur Arbeits- oder Schulkonten kennt Token, Ablauf, Kontotyp
AADSTS65001: „The user or administrator has not consented to use the application …“ Die Einwilligung fehlt. Die Fehlermeldung nennt die App und verlangt eine interaktive Anforderung ob User.Read in der Registrierung steht; ob der Mandant die Einwilligung der Benutzer ausgeschaltet hat; ob Benutzerzuweisung nötig ist; ob der Dienstprinzipal der App im Mandanten existiert
AADSTS90094 und AADSTS90093 Die App verlangt Berechtigungen, die eine Person nicht erteilen darf, oder eine Richtlinie verbietet es die Administration zustimmen lassen
AADSTS700016 Eine App mit dieser ID gibt es im Mandanten nicht client_id und Mandant vergleichen
403 mit Authorization_RequestDenied („Insufficient privileges to complete the operation“) Dem Token fehlt eine Berechtigung, oder dem Aufrufer fehlt eine nötige Rolle Berechtigungen in der Referenz, in Entra und im Token; bei Verzeichnisdaten die Entra-Rolle
403, Token von der falschen Art Anwendungsrechte vergeben, aber ein delegiertes Token benutzt (oder umgekehrt) scp und roles im Token
403, Person ohne Recht oder ohne Lizenz bei delegiert zählt der Schnitt; fehlt der Person das Recht oder die Lizenz für die Daten, scheitert der Aufruf die Person im Postfach prüfen
403, falsche Zielgruppe (aud) Das Token wurde für eine andere API ausgestellt (etwa für die ältere Azure AD Graph oder die Outlook-APIs) den Bereich, für den das Token geholt wurde
403 mit insufficient_claims, oder 400 mit interaction_required Eine Richtlinie für bedingten Zugriff verlangt mehr (Mehrstufige Anmeldung, Geräte) Learn: Handling conditional access challenges
403 mit ErrorAccessDenied und „Access to OData is disabled.“ Exchange Online: eine EWS-Zugriffsrichtlinie der Organisation blockiert Get-OrganizationConfig \| fl EwsApplicationAccessPolicy,EWS*List, und Get-CASMailbox <upn> \| fl …; bei EnforceAllowList muss der User-Agent der App in EwsAllowList stehen, bei EnforceBlockList aus EwsBlockList entfernt werden; die Änderung wirkt verzögert

Eine Gemeinsamkeit: Das Fehlerobjekt von Graph (error.code, error.message, innerError) ist, was Sie auswerten. Learn sagt dazu: Programmieren Sie gegen die Fehlercodes in code, nie gegen den Text in message; der ändert sich und ist nicht lokalisiert. Und: Nehmen Sie bei verschachtelten innerError den genauesten Code, den Sie kennen. Die request-id im Fehlerobjekt gehört in Ihr Protokoll; sie führt den Support zur Anfrage (Lektion W4.3.2).

Die Fehlermeldungen enthalten oft Namen und IDs, aber nie Geheimnisse; trotzdem gehört eine Fehlermeldung nicht ungeprüft auf eine Webseite für fremde Leute.

Tests ohne Mandanten

Diese Lektion hat zwei Dinge, die sich ohne Mandanten prüfen lassen: das Lesen eines Tokens und die Reaktion auf einen Berechtigungsfehler. Der erste Test baut die Token selbst. Er braucht keine Signatur, weil TokenPermissions keine prüft.

C#
// tests/Kladde.GraphLab.Tests/TokenPermissionsTests.cs
using System.Buffers.Text;
using System.Text;
using Shouldly;
using Xunit;

namespace Kladde.GraphLab.Tests;

public sealed class TokenPermissionsTests
{
    [Fact]
    public void AScopeClaimMakesItADelegatedToken()
    {
        TokenPermissions token = TokenPermissions.Read(Jwt("""{ "scp": "Directory.Read.All User.Read", "sub": "<sub>" }"""));

        token.Kind.ShouldBe(TokenKind.Delegated);
        token.Permissions.ShouldBe(["Directory.Read.All", "User.Read"]);
    }

    [Fact]
    public void RolesWithoutAScopeMakeItAnApplicationToken()
    {
        TokenPermissions token = TokenPermissions.Read(Jwt("""{ "oid": "<oid>", "roles": [ "User.Read.All" ], "sub": "<sub>" }"""));

        token.Kind.ShouldBe(TokenKind.Application);
        token.Permissions.ShouldBe(["User.Read.All"]);
    }

    [Fact]
    public void RolesInAUserTokenAreNotThePermissions()
    {
        // A user token may list the person's roles in the API next to the scopes; the scopes are what the app may do.
        TokenPermissions token = TokenPermissions.Read(Jwt("""{ "scp": "User.Read", "roles": [ "Approver" ] }"""));

        token.Kind.ShouldBe(TokenKind.Delegated);
        token.Permissions.ShouldBe(["User.Read"]);
    }

    [Fact]
    public void AnAppTokenWithoutRolesIsAnApplicationWithoutPermissions()
    {
        TokenPermissions token = TokenPermissions.Read(Jwt("""{ "idtyp": "app" }"""));

        token.Kind.ShouldBe(TokenKind.Application);
        token.Permissions.ShouldBeEmpty();
    }

    [Theory]
    [InlineData("")]
    [InlineData("not-a-token")]
    [InlineData("a.b.c")]
    public void SomethingThatIsNoTokenIsUnknown(string text)
    {
        TokenPermissions.Read(text).Kind.ShouldBe(TokenKind.Unknown);
    }

    // header.payload.signature, the first two parts Base64Url of JSON, the signature a placeholder nobody checks.
    private static string Jwt(string payloadJson)
    {
        string header = Base64Url.EncodeToString(Encoding.UTF8.GetBytes("""{ "alg": "none" }"""));
        string payload = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(payloadJson));
        return $"{header}.{payload}.signature";
    }
}

Die Token-Auszüge in den Tests sind die von Learn (scp: "Directory.Read.All User.Read", roles: ["User.Read.All"]), mit Platzhaltern für die IDs. Der Test SomethingThatIsNoTokenIsUnknown mit "a.b.c" prüft den Fall, dass b kein gültiges Base64-URL oder kein JSON ist. Erwartet wird Unknown, kein Absturz.

Der zweite Test zeigt, was das SDK aus einem 403 macht, und braucht den FakeGraphHandler aus Lektion W4.1.1:

C#
// tests/Kladde.GraphLab.Tests/WhoAmIForbiddenTests.cs
using System.Net;
using Microsoft.Graph.Models.ODataErrors;
using Shouldly;
using Xunit;

namespace Kladde.GraphLab.Tests;

public sealed class WhoAmIForbiddenTests
{
    [Fact]
    public async Task AForbiddenAnswerSurfacesAsAnODataError()
    {
        FakeGraphHandler fake = FakeGraphHandler.Json("""
            {
              "error": {
                "code": "Authorization_RequestDenied",
                "message": "Insufficient privileges to complete the operation.",
                "innerError": { "request-id": "<request-id>", "date": "2025-11-05T08:00:00" }
              }
            }
            """, HttpStatusCode.Forbidden);
        WhoAmI who = new(GraphTestClient.With(fake));

        await Should.ThrowAsync<ODataError>(() => who.DescribeAsync());
    }
}

Das Fehlerobjekt ist das von Learns Seite zu Fehlerantworten, mit Platzhalter für die request-id. Offen: Dass das SDK bei 403 einen ODataError wirft, nennt eine Learn-Seite zu Microsoft.Identity.Web („Catch ODataError“); der Namensraum Microsoft.Graph.Models.ODataErrors und die Eigenschaften des Fehlers (etwa der Code in Error) stehen nicht auf den gelesenen Seiten. Lesen Sie sie in Ihrer IDE nach, bevor Sie darauf programmieren. Und ob ein Test, der nur den Typ prüft, genug ist: Er beweist nicht, dass die App mit dem Fehler klug umgeht (eine verständliche Meldung, eine Wiederholung nur dort, wo sie erlaubt ist). Das ist Aufgabe der Lektion W4.3.2.

Zuletzt eine kleine, reine Funktion, die aus einem Fehlercode einen Hinweis macht. Sie hat keine Abhängigkeit vom SDK und ist deshalb leicht zu testen:

C#
// src/Kladde.GraphLab/AuthorizationHints.cs
namespace Kladde.GraphLab;

/// <summary>A German hint for the codes of the table "Typische Fehler" (Microsoft Learn: resolve Graph authorization errors).</summary>
public static class AuthorizationHints
{
    public static string? For(string? code) => code switch
    {
        "AADSTS65001" => "Einwilligung fehlt: Eine Person oder die Administration muss den Berechtigungen der App zustimmen.",
        "AADSTS90093" or "AADSTS90094" => "Diese Berechtigungen darf eine Person nicht erteilen: Die Administration muss zustimmen.",
        "AADSTS700016" => "Die App gibt es im Mandanten nicht: client_id und Mandant prüfen.",
        "Authorization_RequestDenied" => "403: Berechtigung oder Rolle fehlt. Berechtigungen der App, Token (scp, roles) und Rollen der Person prüfen.",
        "ErrorAccessDenied" => "403 von Exchange Online: Token (roles), Postfach-Eingrenzung und EWS-Zugriffsrichtlinie prüfen.",
        _ => null,
    };
}

Übung 1: Die Rechteliste schreiben (ohne Mandanten)

Die Rechteliste der Kladde, mit Begründung je Zeile. Diese Übung braucht keinen Mandanten. Sie brauchen die Referenzseiten von Learn.

  1. Öffnen Sie die Referenzseiten von calendarView, sendMail, „Create event“ und „Get a user“ (Adressen in „Weiterlesen“). Lesen Sie jeweils die Tabelle Permissions.
  2. Schreiben Sie eine Tabelle mit den Spalten Aufgabe, Art, Berechtigung, warum nicht weniger, Eingrenzung und vier Zeilen: Bestätigungsmail senden; Termine des Büros lesen und schreiben; „meine Termine heute“ in einem Werkzeug; Profil der angemeldeten Person anzeigen.
  3. Entscheiden Sie bei jeder Zeile, ob delegiert oder Anwendung. Schreiben Sie dazu einen Satz.
  4. Entscheiden Sie bei den Zeilen mit Anwendungsrechten, wo die Berechtigung vergeben wird, in Entra ID oder in Exchange, und warum.
  5. Schreiben Sie darunter, was ein Kundenmandant tun muss, damit die Kladde läuft, in drei Zeilen: wer zustimmt, welche Rolle dafür nötig ist und welche Adresse Sie ihm schicken.

So prüfen Sie: Keine Zeile nennt eine Berechtigung mit .All, wo eine kleinere reicht; jede Zeile mit Anwendungsrechten nennt eine Eingrenzung; die Zeile für das Werkzeug ist delegiert; und Sie nennen für die Zustimmung zu Graph-Anwendungsberechtigungen die Rolle Privileged Role Administrator. Ihre Antwort muss nicht wörtlich wie die Lösung sein, aber jede Abweichung braucht einen Satz Begründung.

Aufräumen: nichts, es ist eine Schreibübung.

Lösung 1

Aufgabe Art Berechtigung Warum nicht weniger Eingrenzung
Bestätigungsmail senden Anwendung Mail.Send der Dienst läuft ohne Person; für sendMail nennt Learn nichts Kleineres RBAC for Applications, Rolle Application Mail.Send, Geltungsbereich auf das Postfach buero
Termine des Büros lesen und schreiben Anwendung Calendars.ReadWrite wir legen Termine an; Learns Beispiel sagt: Lesen und Schreiben zusammen brauchen die höhere Berechtigung RBAC for Applications, Rolle Application Calendars.ReadWrite, Geltungsbereich auf die Büro-Postfächer
„meine Termine heute“ im Werkzeug delegiert User.Read, Calendars.Read (Prüfauftrag: Calendars.ReadBasic) die Person sieht nur ihre Termine, die Berechtigung ist die für calendarView ohne Schreiben durch das Delegiertenmodell
Profil der angemeldeten Person delegiert User.Read kleinste für GET /me durch das Delegiertenmodell
  • Wo die Berechtigung vergeben wird: Die Anwendungsrechte auf Mail und Kalender in Exchange (RBAC for Applications), nicht in Entra ID, weil sich beide Quellen addieren; eine Zustimmung in Entra ID würde die Eingrenzung aufheben.
  • Kundenmandant: (1) Die Administration stimmt zu: für Graph-Anwendungsberechtigungen verlangt Learn einen Privileged Role Administrator, für delegierte Berechtigungen reicht unter anderem ein Cloud Application Administrator. (2) Ein Exchange-Administrator richtet die Eingrenzung ein (Organization Management in Exchange, Exchange Administrator in Entra). (3) Sie schicken ihm die Liste mit Begründung und die Adresse https://login.microsoftonline.com/<tenant-id>/adminconsent?client_id=<client-id>, nie ein Geheimnis.

Häufige Abweichungen:

  • „Calendars.ReadBasic genügt für alles“: Die Rolle Application Calendars.ReadBasic gibt es in der Liste der Exchange-Anwendungsrollen nicht (nur Calendars.Read und Calendars.ReadWrite). Wer Kalender in Exchange eingrenzen will, hat also nicht die Wahl.
  • „Eine Registrierung für alles“: Das geht, aber Sie können die Rechte dann nicht getrennt einschränken oder widerrufen (siehe Abschnitt zur Registrierung).
  • „Mail.ReadWrite für das Senden“: Mail.ReadWrite enthält nach Learns Permissions reference ausdrücklich keine Berechtigung zum Senden.

Übung 2: Ein Token lesen (ohne Mandanten)

TokenPermissions mit Tests bauen. Diese Übung braucht keinen Mandanten.

  1. Schreiben Sie TokenPermissions wie in der Lektion in Kladde.GraphLab (die Datei TokenPermissions.cs) und die Tests aus der Lektion in TokenPermissionsTests.
  2. Ergänzen Sie einen Test TheTokenItselfIsNeverPartOfTheResult, der prüft, dass weder der Text des Tokens noch die Signatur in ToString() des Ergebnisses vorkommen. Bauen Sie dazu ein Token mit der Signatur geheim-nicht-ausgeben und prüfen Sie, dass der Text nicht in result.ToString() steht.
  3. Lassen Sie die Tests laufen und lesen Sie den Exit-Code.
  4. Gegenprobe: Ändern Sie in Read die Reihenfolge so, dass roles vor scp geprüft wird, und starten Sie die Tests. Der Test RolesInAUserTokenAreNotThePermissions muss rot werden. Machen Sie die Änderung rückgängig.

So prüfen Sie: Alle Tests sind grün, $LASTEXITCODE ist 0; nach der Gegenprobe ist genau der genannte Test rot.

Aufräumen: die Gegenprobe zurücknehmen; keine Datei außerhalb des Projekts wurde angelegt.

Lösung 2

C#
    [Fact]
    public void TheTokenItselfIsNeverPartOfTheResult()
    {
        string jwt = Jwt("""{ "scp": "User.Read" }""").Replace(".signature", ".geheim-nicht-ausgeben", StringComparison.Ordinal);

        string text = TokenPermissions.Read(jwt).ToString();

        text.ShouldNotContain("geheim-nicht-ausgeben");
        text.ShouldNotContain(jwt);
    }

Der Test ist ein Beispiel für „Der Test beweist, was die Klasse nicht tut“. Weil TokenPermissions ein record ist, steht in ToString() alles, was im Record steht (Kind und Permissions); solange nur die Berechtigungen im Record stehen, ist der Test grün. Er wird rot, sobald jemand ein Feld Raw ergänzt, und das ist sein Sinn.

Häufige Fehler:

  • ToString() zeigt Permissions = System.String[]: Das ist die Standarddarstellung einer Liste im record. Für den Test genügt es; für eine lesbare Ausgabe schreiben Sie in der Konsole string.Join(", ", token.Permissions).
  • Der Test für a.b.c wirft statt Unknown zu liefern: Das Fangen von FormatException und JsonException fehlt; prüfen Sie den catch.
  • Base64Url ist unbekannt: Der Namensraum System.Buffers.Text fehlt, oder das Projekt zielt nicht auf .NET 9 oder neuer.

Übung 3: Ein 403 und ein Hinweis (ohne Mandanten)

Das Verhalten bei einem Berechtigungsfehler prüfen. Diese Übung braucht keinen Mandanten.

  1. Schreiben Sie den Test AForbiddenAnswerSurfacesAsAnODataError aus der Lektion. Er braucht die Klassen FakeGraphHandler und GraphTestClient aus Lektion W4.1.1.
  2. Schreiben Sie AuthorizationHints und einen Test, der für jeden der sechs Codes einen Hinweis erwartet und für "NoSuchThing" und null keinen.
  3. Lassen Sie die Tests laufen, lesen Sie den Exit-Code.
  4. Schreiben Sie einen weiteren Test mit der Antwort 401 und dem Fehlerobjekt { "error": { "code": "InvalidAuthenticationToken", "message": "<text>" } }, der prüft, dass auch hier ein ODataError kommt.

So prüfen Sie: Alle Tests grün, Exit-Code 0. Gegenprobe: Lassen Sie den Fake mit 200 und {} antworten; Should.ThrowAsync muss dann rot werden, weil nichts geworfen wird.

Aufräumen: die Gegenprobe zurücknehmen.

Lösung 3

C#
public sealed class AuthorizationHintsTests
{
    [Theory]
    [InlineData("AADSTS65001")]
    [InlineData("AADSTS90093")]
    [InlineData("AADSTS90094")]
    [InlineData("AADSTS700016")]
    [InlineData("Authorization_RequestDenied")]
    [InlineData("ErrorAccessDenied")]
    public void EveryCodeOfTheTableHasAHint(string code)
    {
        AuthorizationHints.For(code).ShouldNotBeNullOrWhiteSpace();
    }

    [Theory]
    [InlineData("NoSuchThing")]
    [InlineData(null)]
    public void AnUnknownCodeHasNoHint(string? code)
    {
        AuthorizationHints.For(code).ShouldBeNull();
    }
}

Die Theorie hat sechs Zeilen, eine je Code der Tabelle „Typische Fehler“, für den die Funktion einen Hinweis kennt (AADSTS90093 und AADSTS90094 stehen dort in einer Zeile). Der Test für 401 ist dem für 403 gleich, mit HttpStatusCode.Unauthorized und dem anderen Fehlerobjekt.

Fehler: [InlineData(null)] meldet eine Warnung zur Nullbarkeit: Der Parameter muss string? sein. Und der Test mit 200 und {} wirft nicht, weil GetAsync dann einen User mit leeren Eigenschaften liefert: Auch das ist die erwartete Gegenprobe.

Übung 4: Einwilligung erleben (Testmandant)

Dieselbe App einmal als Person und einmal mit Administratorzustimmung nutzen. Diese Übung braucht einen Testmandanten, nie einen Produktivmandanten: Sie ändern Zustimmungen. Sie brauchen dort ein Konto mit der Rolle Cloud Application Administrator oder höher und eine zweite Testperson ohne Verwaltungsrolle. Wer das nicht hat, macht die Übungen 1 bis 3, die denselben Stoff ohne Mandanten behandeln.

  1. Nutzen Sie die Registrierung kladde-graphlab-test aus Übung 4 der Lektion W4.1.1 (oder legen Sie sie nach der dortigen Anleitung neu an). Prüfen Sie in der Registrierung unter API permissions, dass genau User.Read und Calendars.Read (delegiert) stehen und keine Zustimmung für die Organisation erteilt ist (Spalte „Status“: ohne Haken).
  2. Starten Sie Kladde.GraphLab -- me als zweite Testperson (ohne Verwaltungsrolle). Notieren Sie: Kam ein Zustimmungsfenster, kam ein Fehler (welcher AADSTS-Code?), oder lief es durch? Das Ergebnis hängt von den Einstellungen Ihres Testmandanten ab; das ist die Beobachtung der Übung.
  3. Wenn ein Fehler kam: Prüfen Sie ihn gegen die Tabelle „Typische Fehler“. Wenn ein Fenster kam: Lesen Sie die Liste der Berechtigungen und den Herausgeber, und stimmen Sie zu.
  4. Als Administration: Entra ID, App registrations, All applications, die App wählen, API permissions, Grant admin consent. Lesen Sie die Berechtigungen, bevor Sie bestätigen.
  5. Prüfen Sie unter Enterprise apps, All applications, die App, Permissions, dass die zwei Berechtigungen für die Organisation erteilt sind.
  6. Starten Sie Kladde.GraphLab -- me und -- day noch einmal als zweite Testperson.
  7. Schreiben Sie zwei Sätze: Was hat sich beim zweiten Lauf geändert, und welche Einstellung Ihres Mandanten hat es verursacht?

So prüfen Sie: Nach Schritt 4 läuft die Anmeldung der zweiten Person ohne Zustimmungsfenster und ohne AADSTS65001; in Schritt 5 stehen User.Read und Calendars.Read als erteilt. Das haben wir nicht gesehen, nur aus Learn abgeleitet.

Aufräumen: Löschen Sie die App-Registrierung kladde-graphlab-test in der Liste der App-Registrierungen und prüfen Sie dann, dass die Unternehmensanwendung gleichen Namens aus der Liste der Enterprise apps verschwunden ist; sonst löschen Sie sie dort (was Entra beim Löschen einer Registrierung mit der Unternehmensanwendung tut, steht auf den gelesenen Seiten nicht). Entfernen Sie die Umgebungsvariablen GRAPHLAB_CLIENT_ID und GRAPHLAB_TENANT_ID. Löschen Sie, wenn Sie sie nur dafür angelegt haben, die zweite Testperson.

Lösung 4

Typische Beobachtungen und ihre Ursachen:

  • Zustimmungsfenster beim ersten Lauf: Die Benutzereinwilligung ist erlaubt und die Berechtigungen verlangen keine Administratorzustimmung. Das ist der normale Fall bei Entwicklermandanten, aber nicht garantiert.
  • AADSTS65001 oder AADSTS90094: Die Benutzereinwilligung ist ausgeschaltet oder auf bestimmte Berechtigungen beschränkt. Die Administration muss zustimmen (Schritt 4). Das ist der Fall, mit dem Sie beim Kunden rechnen müssen.
  • Nach Schritt 4 weiterhin ein Fenster: Die App hat eine Berechtigung angefordert, der die Administration nicht zugestimmt hat (etwa weil in der Registrierung später eine hinzukam), oder in der Registrierung steht eine andere als in Ihrer Liste.
  • „Grant admin consent“ ist nicht anklickbar: Ihr Konto hat die Rolle nicht. Learn nennt für delegierte Berechtigungen mindestens Cloud Application Administrator.
  • Die zweite Person kann die App nicht nutzen, obwohl zugestimmt ist: Die App steht auf Benutzerzuweisung erforderlich, und die Person ist nicht zugewiesen.

Übung 5: Ein Postfach eingrenzen (Testmandant mit Exchange Online)

Eine Anwendungsrolle auf ein Postfach beschränken und mit dem Testbefehl prüfen. Diese Übung braucht einen Testmandanten mit Exchange Online, nie einen Produktivmandanten, und dort zwei Postfächer (buero und fremd). Ob und zu welchen Bedingungen Sie einen solchen Mandanten bekommen, entscheidet Microsoft. Sie brauchen die Rolle Exchange Administrator in Entra und die Mitgliedschaft in Organization Management in Exchange, und die Objekt-ID der Unternehmensanwendung (nicht die der App-Registrierung) einer Test-App. Der Teil ohne Mandanten ist der Probelauf des Skripts mit -WhatIf in Schritt 1 sowie die Übungen 1 bis 3.

  1. Legen Sie das Skript scripts/runbooks/graph/kladde-mailbox-scope.ps1 aus der Lektion an. Starten Sie es zuerst mit -WhatIf und den Platzhaltern: pwsh -File scripts/runbooks/graph/kladde-mailbox-scope.ps1 -ServicePrincipalAppId <client-id> -ServicePrincipalObjectId <service-principal-object-id> -MailboxAlias buero -WhatIf. Es darf keine Anmeldung auslösen und schreibt für jeden Schritt eine Zeile „What if: …“. Stehen die Zeilen nicht in Ihrer Konsole, halten Sie den Lauf mit Start-Transcript fest.
  2. Installieren Sie bei Bedarf das Modul ExchangeOnlineManagement (nach dem Weg, den Learn für Exchange Online PowerShell beschreibt; haben wir nicht gelesen).
  3. Starten Sie das Skript ohne -WhatIf mit Rolle Application Calendars.Read und Alias buero. Melden Sie sich als Exchange-Administrator an.
  4. Prüfen Sie mit Test-ServicePrincipalAuthorization -Identity <service-principal-object-id> -Resource fremd (das Skript prüft schon buero).
  5. Notieren Sie, was in InScope steht, für beide Postfächer.

So prüfen Sie: Für buero steht InScope auf True, für fremd auf False; beim Schritt 4 zeigt der Test auch die Rolle Application Calendars.Read mit Geltungsbereich Kladde buero. Beides haben wir nicht gesehen. Und: Der Testbefehl ersetzt keinen Aufruf der App: Er prüft die Zuweisungen, nicht, ob Entra der App etwas zusätzlich gegeben hat. Die Zuweisungen in Exchange wirken für die echte App erst nach bis zu zwei Stunden (Zwischenspeicher).

Aufräumen: pwsh -File scripts/runbooks/graph/kladde-mailbox-scope.ps1 ... -MailboxAlias buero -Role 'Application Calendars.Read' -Remove. Dann Remove-ServicePrincipal -Identity <service-principal-object-id>, wenn kein anderer Test den Zeiger braucht, und trennen Sie die Sitzung (Disconnect-ExchangeOnline, nach unserem Kenntnisstand, nicht von Learn gelesen). Die Test-App löschen Sie wie in Übung 4.

Lösung 5

Häufige Fehler:

  • New-ServicePrincipal meldet, dass die ID nicht stimmt: Sie haben die Objekt-ID der App-Registrierung oder die Anwendungs-ID als -ObjectId genommen. Gebraucht wird die Objekt-ID des Dienstprinzipals aus den Unternehmensanwendungen (Learn zeigt beide Stellen auf einem Bildschirmfoto); -AppId ist die Client-ID.
  • InScope ist für beide Postfächer False: Der Alias im Filter stimmt nicht mit dem Alias des Postfachs überein, oder das Postfach ist nicht das, das Sie meinen. Prüfen Sie den Filter Alias -eq 'buero'.
  • Der Testbefehl nennt „Not Run“: Sie haben -Resource weggelassen.
  • Die echte App kann trotz InScope: False das Postfach fremd lesen: In Entra ID wurde der App dieselbe Berechtigung zusätzlich zugestimmt. Die Rechte addieren sich. Entfernen Sie die Zustimmung in Entra ID (Unternehmensanwendungen, Permissions).
  • Eine Änderung zeigt keine Wirkung bei der echten App: Der Zwischenspeicher: bis zu zwei Stunden bei einer App mit Aufrufen.
  • Das Skript meldet beim zweiten Lauf, es gebe die Objekte schon: Es prüft nicht, ob sie existieren. Räumen Sie mit -Remove auf und starten Sie neu.

Was Sie jetzt können

  • Sie erklären den Unterschied zwischen delegierten Berechtigungen und Anwendungsberechtigungen, nennen für beide, wie weit der Zugriff reicht, und wissen, warum Learn das delegierte Modell empfiehlt, wo es genügt.
  • Sie sagen, wer zustimmen darf, welche Rolle für Graph-Anwendungsberechtigungen nötig ist, und was die Einstellungen eines Mandanten ändern. Sie schicken einer Administration die Adresse für die Zustimmung und eine Liste mit Begründung, nie ein Geheimnis.
  • Sie registrieren eine App, ordnen Geheimnis, Schlüsselpaar und föderierte Anmeldeinformation ein und begründen, warum die Kladde je Zweck eine Registrierung bekommt.
  • Sie wählen mit den Referenzseiten die kleinste Berechtigung je Aufruf und schreiben die Rechteliste der Kladde mit Begründung je Zeile.
  • Sie lesen scp und roles in einem Token und erkennen, ob es delegiert ist oder zur Anwendung gehört; Sie geben dabei nie das Token selbst aus.
  • Sie beschränken Anwendungsrechte auf Postfächer mit RBAC for Applications, wissen, dass Entra ID und Exchange sich addieren, und lassen ein Skript zuerst mit -WhatIf laufen.
  • Sie ordnen AADSTS65001, Authorization_RequestDenied und ErrorAccessDenied ihren Ursachen zu.

Weiterlesen

Die Kladde: Sie hat jetzt eine Rechteliste mit Begründung je Zeile, drei Registrierungen mit je einem Zweck, ein Skript mit Probelauf für die Eingrenzung des Büro-Postfachs, ein Werkzeug, das ein Token liest, ohne es zu zeigen, und Tests, die ohne Mandanten laufen. In Lektion W4.2.1 schickt die Kladde mit dem SDK ihre erste Mail.