Erstellen von Azure-Funktionen mit Microsoft Graph
In diesem Lernprogramm erfahren Sie, wie Sie eine Azure-Funktion erstellen, die die Microsoft Graph-API verwendet, um Kalenderinformationen für einen Benutzer abzurufen.
Tipp
Wenn Sie es vorziehen, nur das abgeschlossene Lernprogramm herunterzuladen, können Sie das GitHub Repository herunterladen oder klonen. Anweisungen zum Konfigurieren der App mit einer App-ID und einem geheimen Schlüssel finden Sie in der README-Datei im Demoordner .
Voraussetzungen
Bevor Sie mit diesem Lernprogramm beginnen, sollten Sie die folgenden Tools auf Ihrem Entwicklungscomputer installiert haben.
Sie sollten auch über ein Microsoft-Geschäfts-, Schul- oder Unikonto mit Zugriff auf ein globales Administratorkonto in derselben Organisation verfügen. Wenn Sie kein Microsoft-Konto haben, können Sie sich für das Microsoft 365-Entwicklerprogramm registrieren, um ein kostenloses Office 365 Abonnement zu erhalten.
Hinweis
Dieses Lernprogramm wurde mit den folgenden Versionen der oben genannten Tools geschrieben. Die Schritte in diesem Handbuch funktionieren möglicherweise mit anderen Versionen, die jedoch nicht getestet wurden.
- .NET Core SDK 5.0.203
- Azure Functions Core Tools 3.0.3442
- Azure CLI 2.23.0
- ngrok 2.3.40
Feedback
Bitte geben Sie Feedback zu diesem Lernprogramm im GitHub Repository.
Erstellen eines Azure Functions-Projekts
In diesem Lernprogramm erstellen Sie eine einfache Azure-Funktion, die HTTP-Triggerfunktionen implementiert, die Microsoft Graph aufrufen. Diese Funktionen umfassen die folgenden Szenarien:
- Implementiert eine API, um mithilfe der Flussauthentifizierung im Auftrag von Benutzern auf den Posteingang eines Benutzers zuzugreifen.
- Implementiert eine API zum Abonnieren und Kündigen von Benachrichtigungen im Posteingang eines Benutzers mithilfe der Flussauthentifizierung mit Clientanmeldeinformationen .
- Implementiert einen Webhook, um Änderungsbenachrichtigungen von Microsoft Graph zu empfangen und mithilfe des Flusses zur Gewährung von Clientanmeldeinformationen auf Daten zuzugreifen.
Sie erstellen auch eine einfache JavaScript-Einzelseitenanwendung (Single-Page Application, SPA), um die in der Azure-Funktion implementierten APIs aufzurufen.
Erstellen eines Azure Functions-Projekts
Öffnen Sie die Befehlszeilenschnittstelle (CLI) in einem Verzeichnis, in dem Sie das Projekt erstellen möchten. Führen Sie den folgenden Befehl aus.
func init GraphTutorial --worker-runtime dotnetisolated
Ändern Sie das aktuelle Verzeichnis in Ihrer CLI in das GraphTutorial-Verzeichnis , und führen Sie die folgenden Befehle aus, um drei Funktionen im Projekt zu erstellen.
func new --name GetMyNewestMessage --template "HTTP trigger" func new --name SetSubscription --template "HTTP trigger" func new --name Notify --template "HTTP trigger"
Öffnen Sie "local.settings.json" , und fügen Sie der Datei Folgendes hinzu, um CORS von
http://localhost:8080
der URL für die Testanwendung zuzulassen."Host": { "CORS": "http://localhost:8080" }
Führen Sie den folgenden Befehl aus, um das Projekt lokal auszuführen.
func start
Wenn alles funktioniert, wird die folgende Ausgabe angezeigt:
Functions: GetMyNewestMessage: [GET,POST] http://localhost:7071/api/GetMyNewestMessage Notify: [GET,POST] http://localhost:7071/api/Notify SetSubscription: [GET,POST] http://localhost:7071/api/SetSubscription
Stellen Sie sicher, dass die Funktionen ordnungsgemäß funktionieren, indem Sie Ihren Browser öffnen und zu den in der Ausgabe angezeigten Funktions-URLs navigieren. Die folgende Meldung sollte in Ihrem Browser angezeigt werden:
Welcome to Azure Functions!
.
Erstellen einer einzelseitigen Anwendung
Öffnen Sie Ihre CLI in einem Verzeichnis, in dem Sie das Projekt erstellen möchten. Erstellen Sie ein Verzeichnis mit dem Namen "TestClient ", um Ihre HTML- und JavaScript-Dateien zu speichern.
Erstellen Sie eine neue Datei mit dem Namen index.html im Verzeichnis "TestClient", und fügen Sie den folgenden Code hinzu.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <title>Azure Functions Graph Tutorial Test Client</title> <link rel="shortcut icon" href="g-raph.png"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.1/css/all.css" crossorigin="anonymous"> <link href="style.css" rel="stylesheet" type="text/css" /> </head> <body> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <div class="container"> <a href="/" class="navbar-brand">Azure Functions Graph Test Client</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul id="authenticated-nav" class="navbar-nav mr-auto"></ul> <ul class="navbar-nav justify-content-end"> <li class="nav-item"> <a class="nav-link" href="https://developer.microsoft.com/graph/docs/concepts/overview" target="_blank"> <i class="fas fa-external-link-alt mr-1"></i>Docs </a> </li> <li id="account-nav" class="nav-item"></li> </ul> </div> </div> </nav> <main id="main-container" role="main" class="container"> </main> <!-- Bootstrap/jQuery --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> <!-- MSAL --> <script src="https://alcdn.msauth.net/browser/2.0.0/js/msal-browser.min.js" integrity="sha384-n3aacu1eFuIAfS3ZY4WGIZiQG/skqpT+cbeqIwLddpmMWcxWZwYdt+F0PgKyw+m9" crossorigin="anonymous"></script> <script src="config.js"></script> <script src="ui.js"></script> <script src="auth.js"></script> <script src="azurefunctions.js"></script> </body> </html>
Dies definiert das grundlegende Layout der App, einschließlich einer Navigationsleiste. Außerdem wird Folgendes hinzugefügt:
- Bootstrap und das unterstützende JavaScript
- FontAwesome
- Microsoft-Authentifizierungsbibliothek für JavaScript (MSAL.js) 2.0
Tipp
Die Seite enthält ein Favicon(
<link rel="shortcut icon" href="g-raph.png">
). Sie können diese Zeile entfernen oder die g-raph.png-Datei aus GitHub herunterladen.Erstellen Sie eine neue Datei namens "style.css " im Verzeichnis "TestClient", und fügen Sie den folgenden Code hinzu.
body { padding-top: 70px; }
Erstellen Sie eine neue Datei mit dem Namen ui.js im Verzeichnis "TestClient", und fügen Sie den folgenden Code hinzu.
// Select DOM elements to work with const authenticatedNav = document.getElementById('authenticated-nav'); const accountNav = document.getElementById('account-nav'); const mainContainer = document.getElementById('main-container'); const Views = { error: 1, home: 2, message: 3, subscriptions: 4 }; // Helper function to create an element, set class, and add text function createElement(type, className, text) { const element = document.createElement(type); element.className = className; if (text) { const textNode = document.createTextNode(text); element.appendChild(textNode); } return element; } // Show the navigation items that should only show if // the user is signed in function showAuthenticatedNav(user, view) { authenticatedNav.innerHTML = ''; if (user) { // Add message link const messageNav = createElement('li', 'nav-item'); const messageLink = createElement('button', `btn btn-link nav-link${view === Views.message ? ' active' : '' }`, 'Latest Message'); messageLink.setAttribute('onclick', 'getLatestMessage();'); messageNav.appendChild(messageLink); authenticatedNav.appendChild(messageNav); // Add subscriptions link const subscriptionNav = createElement('li', 'nav-item'); const subscriptionLink = createElement('button', `btn btn-link nav-link${view === Views.message ? ' active' : '' }`, 'Subscriptions'); subscriptionLink.setAttribute('onclick', `updatePage(${Views.subscriptions});`); subscriptionNav.appendChild(subscriptionLink); authenticatedNav.appendChild(subscriptionNav); } } // Show the sign in button or the dropdown to sign-out function showAccountNav(user) { accountNav.innerHTML = ''; if (user) { // Show the "signed-in" nav accountNav.className = 'nav-item dropdown'; const dropdown = createElement('a', 'nav-link dropdown-toggle'); dropdown.setAttribute('data-toggle', 'dropdown'); dropdown.setAttribute('role', 'button'); accountNav.appendChild(dropdown); const userIcon = createElement('i', 'far fa-user-circle fa-lg rounded-circle align-self-center'); userIcon.style.width = '32px'; dropdown.appendChild(userIcon); const menu = createElement('div', 'dropdown-menu dropdown-menu-right'); dropdown.appendChild(menu); const userName = createElement('h5', 'dropdown-item-text mb-0', user); menu.appendChild(userName); const divider = createElement('div', 'dropdown-divider'); menu.appendChild(divider); const signOutButton = createElement('button', 'dropdown-item', 'Sign out'); signOutButton.setAttribute('onclick', 'signOut();'); menu.appendChild(signOutButton); } else { // Show a "sign in" button accountNav.className = 'nav-item'; const signInButton = createElement('button', 'btn btn-link nav-link', 'Sign in'); signInButton.setAttribute('onclick', 'signIn();'); accountNav.appendChild(signInButton); } } // Renders the home view function showWelcomeMessage(user) { // Create jumbotron const jumbotron = createElement('div', 'jumbotron'); const heading = createElement('h1', null, 'Azure Functions Graph Tutorial Test Client'); jumbotron.appendChild(heading); const lead = createElement('p', 'lead', 'This sample app is used to test the Azure Functions in the Azure Functions Graph Tutorial'); jumbotron.appendChild(lead); if (user) { // Welcome the user by name const welcomeMessage = createElement('h4', null, `Welcome ${user}!`); jumbotron.appendChild(welcomeMessage); const callToAction = createElement('p', null, 'Use the navigation bar at the top of the page to get started.'); jumbotron.appendChild(callToAction); } else { // Show a sign in button in the jumbotron const signInButton = createElement('button', 'btn btn-primary btn-large', 'Click here to sign in'); signInButton.setAttribute('onclick', 'signIn();') jumbotron.appendChild(signInButton); } mainContainer.innerHTML = ''; mainContainer.appendChild(jumbotron); } // Renders an email message function showLatestMessage(message) { // Show message const messageCard = createElement('div', 'card'); const cardBody = createElement('div', 'card-body'); messageCard.appendChild(cardBody); const subject = createElement('h1', 'card-title', `${message.subject || '(No subject)'}`); cardBody.appendChild(subject); const fromLine = createElement('div', 'd-flex'); cardBody.appendChild(fromLine); const fromLabel = createElement('div', 'mr-3'); fromLabel.appendChild(createElement('strong', '', 'From:')); fromLine.appendChild(fromLabel); fromLine.appendChild(createElement('div', '', message.from.emailAddress.name)); const receivedLine = createElement('div', 'd-flex'); cardBody.appendChild(receivedLine); const receivedLabel = createElement('div', 'mr-3'); receivedLabel.appendChild(createElement('strong', '', 'Received:')); receivedLine.appendChild(receivedLabel); receivedLine.appendChild(createElement('div', '', message.receivedDateTime)); mainContainer.innerHTML = ''; mainContainer.appendChild(messageCard); } // Renders current subscriptions from the session, and allows the user // to add new subscriptions function showSubscriptions() { const subscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')); // Show new subscription form const form = createElement('form', 'form-inline mb-3'); const userInput = createElement('input', 'form-control mb-2 mr-2 flex-grow-1'); userInput.setAttribute('id', 'subscribe-user'); userInput.setAttribute('type', 'text'); userInput.setAttribute('placeholder', 'User to subscribe to (user ID or UPN)'); form.appendChild(userInput); const subscribeButton = createElement('button', 'btn btn-primary mb-2', 'Subscribe'); subscribeButton.setAttribute('type', 'button'); subscribeButton.setAttribute('onclick', 'createSubscription();'); form.appendChild(subscribeButton); const card = createElement('div', 'card'); const cardBody = createElement('div', 'card-body'); card.appendChild(cardBody); cardBody.appendChild(createElement('h2', 'card-title mb-4', 'Existing subscriptions')); const subscriptionTable = createElement('table', 'table'); cardBody.appendChild(subscriptionTable); const thead = createElement('thead', ''); subscriptionTable.appendChild(thead); const theadRow = createElement('tr', ''); thead.appendChild(theadRow); theadRow.appendChild(createElement('th', '')); theadRow.appendChild(createElement('th', '', 'User')); theadRow.appendChild(createElement('th', '', 'Subscription ID')) if (subscriptions) { // List subscriptions for (const subscription of subscriptions) { const row = createElement('tr', ''); subscriptionTable.appendChild(row); const deleteButtonCell = createElement('td', ''); row.appendChild(deleteButtonCell); const deleteButton = createElement('button', 'btn btn-sm btn-primary', 'Delete'); deleteButton.setAttribute('onclick', `deleteSubscription("${subscription.subscriptionId}");`); deleteButtonCell.appendChild(deleteButton); row.appendChild(createElement('td', '', subscription.userId)); row.appendChild(createElement('td', '', subscription.subscriptionId)); } } mainContainer.innerHTML = ''; mainContainer.appendChild(form); mainContainer.appendChild(card); } // Renders an error function showError(error) { const alert = createElement('div', 'alert alert-danger'); const message = createElement('p', 'mb-3', error.message); alert.appendChild(message); if (error.debug) { const pre = createElement('pre', 'alert-pre border bg-light p-2'); alert.appendChild(pre); const code = createElement('code', 'text-break text-wrap', JSON.stringify(error.debug, null, 2)); pre.appendChild(code); } mainContainer.innerHTML = ''; mainContainer.appendChild(alert); } // Re-renders the page with the selected view function updatePage(view, data) { if (!view) { view = Views.home; } // Get the user name from the session const user = sessionStorage.getItem('msal-userName'); if (!user && view !== Views.error) { view = Views.home; } showAccountNav(user); showAuthenticatedNav(user, view); switch (view) { case Views.error: showError(data); break; case Views.home: showWelcomeMessage(user); break; case Views.message: showLatestMessage(data); break; case Views.subscriptions: showSubscriptions(); break; } } updatePage(Views.home);
Dieser Code verwendet JavaScript, um die aktuelle Seite basierend auf der ausgewählten Ansicht zu rendern.
Testen der einzelseitigen Anwendung
Hinweis
Dieser Abschnitt enthält Anweisungen für die Verwendung von dotnet-serve zum Ausführen eines einfachen HTTP-Testservers auf Ihrem Entwicklungscomputer. Die Verwendung dieses bestimmten Tools ist nicht erforderlich. Sie können einen beliebigen Testserver verwenden, den Sie für das TestClient-Verzeichnis bevorzugen.
Führen Sie den folgenden Befehl in Der CLI aus, um dotnet-serve zu installieren.
dotnet tool install --global dotnet-serve
Ändern Sie das aktuelle Verzeichnis in Ihrer CLI in das TestClient-Verzeichnis , und führen Sie den folgenden Befehl aus, um einen HTTP-Server zu starten.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate" -p 8080
Öffnen Sie Ihren Browser und navigieren Sie zu
http://localhost:8080
. Die Seite sollte gerendert werden, aber keine der Schaltflächen funktioniert derzeit.
Hinzufügen von NuGet-Paketen
Installieren Sie vor dem Fortfahren einige zusätzliche NuGet Pakete, die Sie später verwenden werden.
- Microsoft.Azure.Functions.Extensions zum Aktivieren der Abhängigkeitsinjektion im Azure Functions-Projekt.
- Microsoft.Extensions.Configuration.UserSecrets zum Lesen der Anwendungskonfiguration aus dem geheimen .NET-Entwicklungsgeheimnisspeicher.
- Microsoft.Graph zum Aufrufen von Microsoft Graph
- Microsoft.Identity.Client zum Authentifizieren und Verwalten von Token.
- Microsoft.IdentityModel.Protocols.OpenIdConnect zum Abrufen der OpenID-Konfiguration für die Tokenüberprüfung.
- System.IdentityModel.Tokens.Jwt zum Überprüfen von Token, die an die Web-API gesendet werden.
Ändern Sie das aktuelle Verzeichnis in Ihrer CLI in das GraphTutorial-Verzeichnis , und führen Sie die folgenden Befehle aus.
dotnet add package Microsoft.Azure.Functions.Extensions --version 1.1.0 dotnet add package Microsoft.Extensions.Configuration.UserSecrets --version 5.0.0 dotnet add package Microsoft.Graph --version 4.0.0 dotnet add package Microsoft.Identity.Client --version 4.35.1 dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect --version 6.12.0 dotnet add package System.IdentityModel.Tokens.Jwt --version 6.12.0
Registrieren der Apps im Portal
In dieser Übung erstellen Sie drei neue Azure AD Anwendungen mithilfe des Azure Active Directory Admin Centers:
- Eine App-Registrierung für die einzelseitige Anwendung, damit sie Benutzer anmelden und Token abrufen kann, mit denen die Anwendung die Azure-Funktion aufrufen kann.
- Eine App-Registrierung für die Azure-Funktion, mit der sie den Im-Auftrag-von-Fluss verwenden kann, um das von der SPA gesendete Token gegen ein Token auszutauschen, mit dem microsoft Graph aufgerufen werden kann.
- Eine App-Registrierung für den Azure Function-Webhook, mit der der Clientanmeldeinformationsfluss zum Aufrufen von Microsoft Graph ohne Benutzer verwendet werden kann.
Hinweis
In diesem Beispiel sind drei App-Registrierungen erforderlich, da sowohl der Fluss "im Auftrag von" als auch der Fluss mit den Clientanmeldeinformationen implementiert wird. Wenn Ihre Azure-Funktion nur einen dieser Flüsse verwendet, müssen Sie nur die App-Registrierungen erstellen, die diesem Fluss entsprechen.
Öffnen Sie einen Browser, navigieren Sie zum Azure Active Directory Admin Center, und melden Sie sich mit einem administrator der Microsoft 365 Mandantenorganisation an.
Wählen Sie in der linken Navigationsleiste Azure Active Directory aus, und wählen Sie dann App-Registrierungen unter Verwalten aus.
Registrieren einer App für die einzelseitige Anwendung
Wählen Sie Neue Registrierung aus. Legen Sie auf der Seite Anwendung registrieren die Werte wie folgt fest.
- Legen Sie Name auf
Graph Azure Function Test App
fest. - Legen Sie die unterstützten Kontotypen nur auf Konten in diesem Organisationsverzeichnis fest.
- Ändern Sie unter "Umleitungs-URI" die Dropdownliste in eine Einzelseitenanwendung (Single-Page Application, SPA), und legen Sie den Wert auf
http://localhost:8080
fest.
- Legen Sie Name auf
Wählen Sie Registrieren aus. Kopieren Sie auf der Seite Graph Azure Function Test App die Werte der Anwendungs-ID (Client-)ID und der Verzeichnis-ID (Mandanten-ID), und speichern Sie sie. Sie benötigen sie in den späteren Schritten.
Registrieren einer App für die Azure-Funktion
Kehren Sie zu App-Registrierungen zurück, und wählen Sie "Neue Registrierung" aus. Legen Sie auf der Seite Anwendung registrieren die Werte wie folgt fest.
- Legen Sie Name auf
Graph Azure Function
fest. - Legen Sie die unterstützten Kontotypen nur auf Konten in diesem Organisationsverzeichnis fest.
- Lassen Sie den Umleitungs-URI leer.
- Legen Sie Name auf
Wählen Sie Registrieren aus. Kopieren Sie auf der Seite Graph Azure-Funktion den Wert der Anwendungs-ID (Client-ID), und speichern Sie sie, sie benötigen Sie im nächsten Schritt.
Wählen Sie unter Verwalten die Option Zertifikate und Geheime Clientschlüssel aus. Wählen Sie die Schaltfläche Neuen geheimen Clientschlüssel aus. Geben Sie einen Wert in Beschreibung ein, wählen Sie eine der Optionen für Gilt bis aus, und wählen Sie dann Hinzufügen aus.
Kopieren Sie den Wert des geheimen Clientschlüssels, bevor Sie diese Seite verlassen. Sie benötigen ihn im nächsten Schritt.
Wichtig
Dieser geheime Clientschlüssel wird nicht noch einmal angezeigt, stellen Sie daher sicher, dass Sie ihn jetzt kopieren.
Wählen Sie API-Berechtigungen unter "Verwalten" aus. Wählen Sie "Berechtigung hinzufügen" aus.
Wählen Sie Microsoft Graph und dann delegierte Berechtigungen aus. Fügen Sie Mail.Read hinzu , und wählen Sie "Berechtigungen hinzufügen" aus.
Wählen Sie "API verfügbar machen**" unter "Verwalten**" und dann "Bereich hinzufügen" aus.
Akzeptieren Sie den Standard-Anwendungs-ID-URI , und wählen Sie "Speichern" aus, und fahren Sie fort.
Füllen Sie das Formular zum Hinzufügen eines Bereichs wie folgt aus:
- Bereichsname: Mail.Read
- Wer zustimmen können?: Administratoren und Benutzer
- Anzeigename der Administratorzustimmung: Lesen der Postfächer aller Benutzer
- Beschreibung der Administratorzustimmung: Ermöglicht der App, die Postfächer aller Benutzer zu lesen.
- Anzeigename der Zustimmung des Benutzers: Lesen Des Posteingangs
- Beschreibung der Benutzerbewilligung: Ermöglicht der App, Ihren Posteingang zu lesen
- Status: Aktiviert
Klicken Sie auf Bereich hinzufügen.
Kopieren Sie den neuen Bereich, den Sie in späteren Schritten benötigen.
Wählen Sie " Manifest" unter "Verwalten" aus.
Suchen Sie
knownClientApplications
im Manifest, und ersetzen Sie den aktuellen Wert durch[]
[TEST_APP_ID]
, wobeiTEST_APP_ID
sich die Anwendungs-ID des Graph Azure Function Test App-Registrierung befindet. Klicken Sie auf Speichern.
Hinweis
Durch Hinzufügen der App-ID der knownClientApplications
Testanwendung zur Eigenschaft im Manifest der Azure-Funktion kann die Testanwendung einen kombinierten Zustimmungsfluss auslösen. Dies ist erforderlich, damit der Im-Auftrag-von-Fluss funktioniert.
Hinzufügen eines Azure-Funktionsbereichs zum Testen der Anwendungsregistrierung
Kehren Sie zum Graph Azure Function Test App-Registrierung zurück, und wählen Sie API-Berechtigungen unter Verwalten aus. Wählen Sie Berechtigung hinzufügen aus.
Wählen Sie "Meine APIs" und dann " Mehr laden" aus. Wählen Sie Graph Azure-Funktion aus.
Wählen Sie die Berechtigung "Mail.Read " und dann " Berechtigungen hinzufügen" aus.
Entfernen Sie in den konfigurierten Berechtigungen die Berechtigung "User.Read" unter Microsoft Graph, indem Sie rechts neben der Berechtigung das ... und dann die Berechtigung "Entfernen" auswählen. Wählen Sie "Ja", "Entfernen " aus, um dies zu bestätigen.
Registrieren einer App für den Azure Function-Webhook
Kehren Sie zu App-Registrierungen zurück, und wählen Sie "Neue Registrierung" aus. Legen Sie auf der Seite Anwendung registrieren die Werte wie folgt fest.
- Legen Sie Name auf
Graph Azure Function Webhook
fest. - Legen Sie die unterstützten Kontotypen nur auf Konten in diesem Organisationsverzeichnis fest.
- Lassen Sie den Umleitungs-URI leer.
- Legen Sie Name auf
Wählen Sie Registrieren aus. Kopieren Sie auf der Graph Azure Function-Webhook-Seite den Wert der Anwendungs-ID (Client-ID), und speichern Sie ihn. Sie benötigen ihn im nächsten Schritt.
Wählen Sie unter Verwalten die Option Zertifikate und Geheime Clientschlüssel aus. Wählen Sie die Schaltfläche Neuen geheimen Clientschlüssel aus. Geben Sie einen Wert in Beschreibung ein, wählen Sie eine der Optionen für Gilt bis aus, und wählen Sie dann Hinzufügen aus.
Kopieren Sie den Wert des geheimen Clientschlüssels, bevor Sie diese Seite verlassen. Sie benötigen ihn im nächsten Schritt.
Wählen Sie API-Berechtigungen unter "Verwalten" aus. Wählen Sie "Berechtigung hinzufügen" aus.
Wählen Sie Microsoft Graph und dann Anwendungsberechtigungen aus. Fügen Sie "User.Read.All" und "Mail.Read" hinzu, und wählen Sie dann "Berechtigungen hinzufügen" aus.
Entfernen Sie in den konfigurierten Berechtigungen die delegierte User.Read-Berechtigung unter Microsoft Graph, indem Sie die Berechtigung ... rechts neben der Berechtigung auswählen und die Berechtigung "Entfernen" auswählen. Wählen Sie "Ja", "Entfernen " aus, um dies zu bestätigen.
Wählen Sie die Schaltfläche " Administratorzustimmung erteilen für... " und dann "Ja " aus, um die Administratorzustimmung für die konfigurierten Anwendungsberechtigungen zu erteilen. Die Spalte "Status " in der Tabelle "Konfigurierte Berechtigungen" ändert sich in "Erteilt für ...".
Implementieren der API mit Im-Auftrag-von-Authentifizierung
In dieser Übung werden Sie die Implementierung der Azure-Funktion GetMyNewestMessage
abschließen und den Testclient aktualisieren, um die Funktion aufzurufen.
Die Azure-Funktion verwendet den Im-Auftrag-von-Fluss. Die grundlegende Reihenfolge der Ereignisse in diesem Fluss ist:
- Die Testanwendung verwendet einen interaktiven Authentifizierungsfluss, damit sich der Benutzer anmelden und seine Zustimmung erteilen kann. Es ruft ein Token zurück, das auf die Azure-Funktion beschränkt ist. Das Token enthält KEINE Microsoft Graph Bereiche.
- Die Testanwendung ruft die Azure-Funktion auf und sendet ihr Zugriffstoken im
Authorization
Header. - Die Azure-Funktion überprüft das Token und tauscht dieses Token dann gegen ein zweites Zugriffstoken aus, das Microsoft Graph Bereiche enthält.
- Die Azure-Funktion ruft Microsoft Graph im Namen des Benutzers mithilfe des zweiten Zugriffstokens auf.
Wichtig
Um das Speichern der Anwendungs-ID und des geheimen Schlüssels in der Quelle zu vermeiden, verwenden Sie den .NET Secret Manager , um diese Werte zu speichern. Der Geheime Manager dient nur zu Entwicklungszwecken, Produktions-Apps sollten einen vertrauenswürdigen geheimen Manager zum Speichern geheimer Schlüssel verwenden.
Hinzufügen der Authentifizierung zur Einzelseitenanwendung
Beginnen Sie, indem Sie der SPA die Authentifizierung hinzufügen. Dadurch kann die Anwendung ein Zugriffstoken abrufen, das Zugriff zum Aufrufen der Azure-Funktion gewährt. Da es sich um eine SPA handelt, wird der Autorisierungscodefluss mit PKCE verwendet.
Erstellen Sie eine neue Datei im TestClient-Verzeichnis mit dem Namen config.js , und fügen Sie den folgenden Code hinzu.
const msalConfig = { auth: { clientId: 'YOUR_TEST_APP_APP_ID_HERE', authority: 'https://login.microsoftonline.com/YOUR_TENANT_ID_HERE' } }; const msalRequest = { // Scope of the Azure Function scopes: [ 'YOUR_AZURE_FUNCTION_APP_ID_HERE/.default' ] }
Ersetzen Sie
YOUR_TEST_APP_APP_ID_HERE
dies durch die Anwendungs-ID, die Sie im Azure-Portal für die Graph Azure Function Test App erstellt haben. Ersetzen SieYOUR_TENANT_ID_HERE
dies durch den Verzeichnis-ID-Wert (Mandant), den Sie aus dem Azure-Portal kopiert haben. Ersetzen SieYOUR_AZURE_FUNCTION_APP_ID_HERE
dies durch die Anwendungs-ID für die Graph Azure-Funktion.Wichtig
Wenn Sie die Quellcodeverwaltung wie Git verwenden, wäre jetzt ein guter Zeitpunkt, um die config.js-Datei aus der Quellcodeverwaltung auszuschließen, um zu vermeiden, dass Versehentlich Ihre App-IDs und Die Mandanten-ID offengelegt werden.
Erstellen Sie eine neue Datei im TestClient-Verzeichnis mit dem Namen auth.js , und fügen Sie den folgenden Code hinzu.
// Create the main MSAL instance // configuration parameters are located in config.js const msalClient = new msal.PublicClientApplication(msalConfig); async function signIn() { // Login try { // Use MSAL to login const authResult = await msalClient.loginPopup(msalRequest); // Save the account username, needed for token acquisition sessionStorage.setItem('msal-userName', authResult.account.username); // Refresh home page updatePage(Views.home); } catch (error) { console.log(error); updatePage(Views.error, { message: 'Error logging in', debug: error }); } } function signOut() { account = null; sessionStorage.removeItem('msal-userName'); msalClient.logout(); }
Überlegen Sie, was dieser Code bewirkt.
- Es initialisiert eine
PublicClientApplication
Mithilfe der in config.js gespeicherten Werte. - Es wird
loginPopup
verwendet, um den Benutzer mithilfe des Berechtigungsbereichs für die Azure-Funktion anzumelden. - Der Benutzername des Benutzers wird in der Sitzung gespeichert.
Wichtig
Da die App verwendet
loginPopup
wird, müssen Sie möglicherweise den Popupblocker Ihres Browsers ändern, um Popups vonhttp://localhost:8080
zuzulassen.- Es initialisiert eine
Aktualisieren Sie die Seite, und melden Sie sich an. Die Seite sollte mit dem Benutzernamen aktualisiert werden, der angibt, dass die Anmeldung erfolgreich war.
Hinzufügen der Authentifizierung zur Azure-Funktion
In diesem Abschnitt implementieren Sie den Im-Auftrag-von-Fluss in der GetMyNewestMessage
Azure-Funktion, um ein Zugriffstoken zu erhalten, das mit Microsoft Graph kompatibel ist.
Initialisieren Sie den geheimen .NET-Entwicklungsspeicher, indem Sie Ihre CLI in dem Verzeichnis öffnen, das GraphTutorial.csproj enthält, und führen Sie den folgenden Befehl aus.
dotnet user-secrets init
Fügen Sie die Anwendungs-ID, den geheimen Schlüssel und die Mandanten-ID mit den folgenden Befehlen zum geheimen Speicher hinzu. Ersetzen Sie
YOUR_API_FUNCTION_APP_ID_HERE
dies durch die Anwendungs-ID für die Graph Azure-Funktion. Ersetzen SieYOUR_API_FUNCTION_APP_SECRET_HERE
dies durch den Anwendungsschlüssel, den Sie im Azure-Portal für die Graph Azure-Funktion erstellt haben. Ersetzen SieYOUR_TENANT_ID_HERE
dies durch den Verzeichnis-ID-Wert (Mandant), den Sie aus dem Azure-Portal kopiert haben.dotnet user-secrets set apiFunctionId "YOUR_API_FUNCTION_APP_ID_HERE" dotnet user-secrets set apiFunctionSecret "YOUR_API_FUNCTION_APP_SECRET_HERE" dotnet user-secrets set tenantId "YOUR_TENANT_ID_HERE"
Verarbeiten des eingehenden Bearertokens
In diesem Abschnitt implementieren Sie eine Klasse zum Überprüfen und Verarbeiten des Bearertokens, das von der SPA an die Azure-Funktion gesendet wurde.
Erstellen Sie ein neues Verzeichnis im GraphTutorial-Verzeichnis mit dem Namen "Authentifizierung".
Erstellen Sie eine neue Datei namens TokenValidationResult.cs im Ordner ./GraphTutorial/Authentication , und fügen Sie den folgenden Code hinzu.
namespace GraphTutorial.Authentication { public class TokenValidationResult { // MSAL account ID - used to access the token // cache public string MsalAccountId { get; private set; } // The extracted token - used to build user assertion // for OBO flow public string Token { get; private set; } public TokenValidationResult(string msalAccountId, string token) { MsalAccountId = msalAccountId; Token = token; } } }
Erstellen Sie eine neue Datei namens "TokenValidation.cs " im Ordner "./GraphTutorial/Authentication ", und fügen Sie den folgenden Code hinzu.
using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using System; using System.Security.Claims; using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public static class TokenValidation { private static TokenValidationParameters _validationParameters = null; public static async Task<TokenValidationResult> ValidateAuthorizationHeader( HttpRequest request, string tenantId, string expectedAudience, ILogger log) { // Check for Authorization header if (request.Headers.ContainsKey("authorization")) { var authHeader = AuthenticationHeaderValue.Parse(request.Headers["authorization"]); if (authHeader != null && authHeader.Scheme.ToLower() == "bearer" && !string.IsNullOrEmpty(authHeader.Parameter)) { if (_validationParameters == null) { // Load the tenant-specific OpenID config from Azure var configManager = new ConfigurationManager<OpenIdConnectConfiguration>( $"https://login.microsoftonline.com/{tenantId}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever()); var config = await configManager.GetConfigurationAsync(); _validationParameters = new TokenValidationParameters { // Use signing keys retrieved from Azure IssuerSigningKeys = config.SigningKeys, ValidateAudience = true, // Audience MUST be the app ID for the Web API ValidAudience = expectedAudience, ValidateIssuer = true, // Use the issuer retrieved from Azure ValidIssuer = config.Issuer, ValidateLifetime = true }; } var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken jwtToken; try { // Validate the token var result = tokenHandler.ValidateToken(authHeader.Parameter, _validationParameters, out jwtToken); // If ValidateToken did not throw an exception, token is valid. return new TokenValidationResult(GetMsalAccountId(result), authHeader.Parameter); } catch (Exception exception) { log.LogError(exception, "Error validating bearer token"); } } } return null; } // Helper function to construct an MSAL account ID from the // claims in the token. MSAL uses an ID in the format // oid.tid, where oid is the object ID of the user, and tid is // the tenant ID. private static string GetMsalAccountId(ClaimsPrincipal principal) { var objectId = principal?.FindFirst("oid"); if (objectId == null) { objectId = principal?.FindFirst( "http://schemas.microsoft.com/identity/claims/objectidentifier"); } var tenantId = principal?.FindFirst("tid"); if (tenantId == null) { tenantId = principal?.FindFirst( "http://schemas.microsoft.com/identity/claims/tenantid"); } if (objectId != null && tenantId != null) { return $"{objectId.Value}.{tenantId.Value}"; } return null; } } }
Überlegen Sie, was dieser Code bewirkt.
- Es wird sichergestellt, dass ein Bearertoken im
Authorization
Header vorhanden ist. - Es überprüft die Signatur und den Aussteller aus der veröffentlichten OpenID-Konfiguration von Azure.
- Es wird überprüft, ob die Zielgruppe (
aud
Anspruch) mit der Anwendungs-ID der Azure-Funktion übereinstimmt. - Es analysiert das Token und generiert eine MSAL-Konto-ID, die benötigt wird, um die Vorteile der Tokenzwischenspeicherung zu nutzen.
Erstellen eines Authentifizierungsanbieters im Auftrag von
Erstellen Sie eine neue Datei im Authentifizierungsverzeichnis " OnBehalfOfAuthProvider.cs" , und fügen Sie der Datei den folgenden Code hinzu.
using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Identity.Client; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public class OnBehalfOfAuthProvider : IAuthenticationProvider { private IConfidentialClientApplication _msalClient; private TokenValidationResult _tokenResult; private string[] _scopes; private ILogger _logger; public OnBehalfOfAuthProvider( IConfidentialClientApplication msalClient, TokenValidationResult tokenResult, string[] scopes, ILogger logger) { _scopes = scopes; _logger = logger; _tokenResult = tokenResult; _msalClient = msalClient; } public async Task<string> GetAccessToken() { try { // First attempt to get token from the cache for this user // Check for a matching account in the cache var account = await _msalClient.GetAccountAsync(_tokenResult.MsalAccountId); if (account != null) { // Make a "silent" request for a token. This will // return the cached token if still valid, and will handle // refreshing the token if needed var cacheResult = await _msalClient .AcquireTokenSilent(_scopes, account) .ExecuteAsync(); _logger.LogInformation($"User access token: {cacheResult.AccessToken}"); return cacheResult.AccessToken; } } catch (MsalUiRequiredException) { // This exception indicates that a new token // can only be obtained by invoking the on-behalf-of // flow. "UiRequired" isn't really accurate since the OBO // flow doesn't involve UI. // Catching the exception so code will continue to the // AcquireTokenOnBehalfOf call below. } catch (Exception exception) { _logger.LogError(exception, "Error getting access token via on-behalf-of flow"); return null; } try { _logger.LogInformation("Token not found in cache, attempting OBO flow"); // Use the token sent by the calling client as a // user assertion var userAssertion = new UserAssertion(_tokenResult.Token); // Invoke on-behalf-of flow var result = await _msalClient .AcquireTokenOnBehalfOf(_scopes, userAssertion) .ExecuteAsync(); _logger.LogInformation($"User access token: {result.AccessToken}"); return result.AccessToken; } catch (Exception exception) { _logger.LogError(exception, "Error getting access token from cache"); return null; } } // This is the delegate called by the GraphServiceClient on each // request. public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage) { // Get the current access token var token = await GetAccessToken(); // Add the token in the Authorization header requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } } }
Nehmen Sie sich einen Moment Zeit, um zu überlegen, was der Code in "OnBehalfOfAuthProvider.cs " bewirkt.
- In der
GetAccessToken
Funktion wird zunächst versucht, ein Benutzertoken mithilfeAcquireTokenSilent
von . Wenn dies fehlschlägt, wird das von der Test-App an die Azure-Funktion gesendete Bearertoken verwendet, um eine Benutzer assertion zu generieren. Anschließend wird diese Benutzer assertion verwendet, um ein Graph-kompatibles Token mithilfeAcquireTokenOnBehalfOf
von abzurufen. - Sie implementiert die
Microsoft.Graph.IAuthenticationProvider
Schnittstelle, sodass diese Klasse im Konstruktor derGraphServiceClient
zum Authentifizieren ausgehender Anforderungen übergeben werden kann.
Implementieren eines Graph-Clientdiensts
In diesem Abschnitt implementieren Sie einen Dienst, der für die Abhängigkeitsinjektion registriert werden kann. Der Dienst wird verwendet, um einen authentifizierten Graph-Client abzurufen.
Erstellen Sie ein neues Verzeichnis im GraphTutorial-Verzeichnis namens "Dienste".
Erstellen Sie eine neue Datei im Verzeichnis "Services " mit dem Namen "IGraphClientService.cs", und fügen Sie der Datei den folgenden Code hinzu.
using GraphTutorial.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Graph; namespace GraphTutorial.Services { public interface IGraphClientService { GraphServiceClient GetUserGraphClient( TokenValidationResult validation, string[] scopes, ILogger logger); GraphServiceClient GetAppGraphClient(ILogger logger); } }
Erstellen Sie eine neue Datei im Verzeichnis "Services" mit dem Namen "GraphClientService.cs", und fügen Sie der Datei den folgenden Code hinzu.
using GraphTutorial.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Identity.Client; using Microsoft.Graph; namespace GraphTutorial.Services { // Service added via dependency injection // Used to get an authenticated Graph client public class GraphClientService : IGraphClientService { } }
Fügen Sie der Klasse die
GraphClientService
folgenden Eigenschaften hinzu.// Configuration private IConfiguration _config; // Single MSAL client object used for all user-related // requests. Making this a "singleton" here because the sample // uses the default in-memory token cache. private IConfidentialClientApplication _userMsalClient;
Fügen Sie der Klasse die
GraphClientService
folgenden Funktionen hinzu.public GraphClientService(IConfiguration config) { _config = config; } public GraphServiceClient GetUserGraphClient(TokenValidationResult validation, string[] scopes, ILogger logger) { // Only create the MSAL client once if (_userMsalClient == null) { _userMsalClient = ConfidentialClientApplicationBuilder .Create(_config["apiFunctionId"]) .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true) .WithTenantId(_config["tenantId"]) .WithClientSecret(_config["apiFunctionSecret"]) .Build(); } // Create a new OBO auth provider for the specific user var authProvider = new OnBehalfOfAuthProvider(_userMsalClient, validation, scopes, logger); // Return a GraphServiceClient initialized with the auth provider return new GraphServiceClient(authProvider); }
Fügen Sie eine Platzhalterimplementierung für die
GetAppGraphClient
Funktion hinzu. Sie werden dies in späteren Abschnitten implementieren.public GraphServiceClient GetAppGraphClient() { throw new System.NotImplementedException(); }
Die
GetUserGraphClient
Funktion übernimmt die Ergebnisse der Tokenüberprüfung und erstellt eine authentifizierteGraphServiceClient
für den Benutzer.Öffnen Sie ./GraphTutorial/Program.cs , und ersetzen Sie den Inhalt durch Folgendes.
Dieser Code fügt der Konfiguration geheime Benutzerschlüssel hinzu und aktiviert die Abhängigkeitsinjektion in Ihren Azure-Funktionen, wodurch der
GraphClientService
Dienst verfügbar wird.
Implementieren der GetMyNewestMessage-Funktion
Öffnen Sie ./GraphTutorial/GetMyNewestMessage.cs , und ersetzen Sie den gesamten Inhalt durch Folgendes.
using GraphTutorial.Authentication; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class GetMyNewestMessage { private IConfiguration _config; private IGraphClientService _clientService; public GetMyNewestMessage(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("GetMyNewestMessage")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["apiFunctionId"]) || string.IsNullOrEmpty(_config["apiFunctionSecret"]) || string.IsNullOrEmpty(_config["tenantId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } // Validate the bearer token var validationResult = await TokenValidation.ValidateAuthorizationHeader( req, _config["tenantId"], _config["apiFunctionId"], log); // If token wasn't returned it isn't valid if (validationResult == null) { return new UnauthorizedResult(); } // Initialize a Graph client for this user var graphClient = _clientService.GetUserGraphClient(validationResult, new[] { "https://graph.microsoft.com/.default" }, log); // Get the user's newest message in inbox // GET /me/mailfolders/inbox/messages var messagePage = await graphClient.Me .MailFolders .Inbox .Messages .Request() // Limit the fields returned .Select(m => new { m.From, m.ReceivedDateTime, m.Subject }) // Sort by received time, newest on top .OrderBy("receivedDateTime DESC") // Only get back one message .Top(1) .GetAsync(); if (messagePage.CurrentPage.Count < 1) { return new OkObjectResult(null); } // Return the message in the response return new OkObjectResult(messagePage.CurrentPage[0]); } } }
Überprüfen des Codes in "GetMyNewestMessage.cs"
Nehmen Sie sich einen Moment Zeit, um zu überlegen, was der Code in "GetMyNewestMessage.cs " bewirkt.
- Im Konstruktor werden die objekte
IGraphClientService
gespeichert, dieIConfiguration
über die Abhängigkeitsinjektion übergeben werden. - In der
Run
Funktion wird Folgendes ausgeführt:- Überprüft, ob die erforderlichen Konfigurationswerte im
IConfiguration
Objekt vorhanden sind. - Überprüft das Bearertoken und gibt einen
401
Statuscode zurück, wenn das Token ungültig ist. - Ruft einen Graph Client für den
GraphClientService
Benutzer ab, der diese Anforderung ausgeführt hat. - Verwendet das Microsoft Graph SDK, um die neueste Nachricht aus dem Posteingang des Benutzers abzurufen, und gibt sie als JSON-Text in der Antwort zurück.
- Überprüft, ob die erforderlichen Konfigurationswerte im
Aufrufen der Azure-Funktion aus der Test-App
Öffnen Sieauth.js , und fügen Sie die folgende Funktion hinzu, um ein Zugriffstoken abzurufen.
async function getToken() { let account = sessionStorage.getItem('msal-userName'); if (!account){ throw new Error( 'User account missing from session. Please sign out and sign in again.'); } try { // First, attempt to get the token silently const silentRequest = { scopes: msalRequest.scopes, account: msalClient.getAccountByUsername(account) }; const silentResult = await msalClient.acquireTokenSilent(silentRequest); return silentResult.accessToken; } catch (silentError) { // If silent requests fails with InteractionRequiredAuthError, // attempt to get the token interactively if (silentError instanceof msal.InteractionRequiredAuthError) { const interactiveResult = await msalClient.acquireTokenPopup(msalRequest); return interactiveResult.accessToken; } else { throw silentError; } } }
Überlegen Sie, was dieser Code bewirkt.
- Zunächst wird versucht, ein Zugriffstoken im Hintergrund ohne Benutzerinteraktion abzurufen. Da der Benutzer bereits angemeldet sein sollte, sollte MSAL Token für den Benutzer im Cache haben.
- Wenn dies mit einem Fehler fehlschlägt, der angibt, dass der Benutzer interagieren muss, wird versucht, ein Token interaktiv abzurufen.
Tipp
Sie können das Zugriffstoken analysieren https://jwt.ms und bestätigen, dass der
aud
Anspruch die App-ID für die Azure-Funktion ist und dass der Anspruch denscp
Berechtigungsbereich der Azure-Funktion enthält, nicht Microsoft Graph.Erstellen Sie eine neue Datei im TestClient-Verzeichnis mit dem Namen azurefunctions.js , und fügen Sie den folgenden Code hinzu.
async function getLatestMessage() { const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } try { const response = await fetch('http://localhost:7071/api/GetMyNewestMessage', { headers: { Authorization: `Bearer ${token}` } }); const message = await response.json(); updatePage(Views.message, message); } catch (error) { updatePage(Views.error, { message: 'Error getting message', debug: error }); } }
Ändern Sie das aktuelle Verzeichnis in Ihrer CLI in das Verzeichnis ./GraphTutorial , und führen Sie den folgenden Befehl aus, um die Azure-Funktion lokal zu starten.
func start
Wenn die SPA nicht bereits bedient wird, öffnen Sie ein zweites CLI-Fenster, und ändern Sie das aktuelle Verzeichnis in das Verzeichnis ./TestClient . Führen Sie den folgenden Befehl aus, um die Testanwendung auszuführen.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate"
Öffnen Sie Ihren Browser und navigieren Sie zu
http://localhost:8080
. Melden Sie sich an, und wählen Sie das Navigationselement "Neueste Nachricht " aus. Die App zeigt Informationen zur neuesten Nachricht im Posteingang des Benutzers an.
Implementieren des Webhooks mit Clientanmeldeinformationsauthentifizierung
In dieser Übung werden Sie die Implementierung der Azure-Funktionen SetSubscription
abschließen und Notify
die Testanwendung aktualisieren, um Änderungen im Posteingang eines Benutzers zu abonnieren und abzubestellen.
- Die
SetSubscription
Funktion fungiert als API, sodass die Test-App ein Abonnement für Änderungen im Posteingang eines Benutzers erstellen oder löschen kann. - Die
Notify
Funktion fungiert als Webhook, der vom Abonnement generierte Änderungsbenachrichtigungen empfängt.
Beide Funktionen verwenden den Fluss zur Gewährung von Clientanmeldeinformationen, um ein Nur-App-Token zum Aufrufen von Microsoft Graph abzurufen. Da ein Administrator die Administratorzustimmung zu den erforderlichen Berechtigungsbereichen erteilt hat, ist keine Benutzerinteraktion erforderlich, um das Token abzurufen.
Hinzufügen der Authentifizierung mit Clientanmeldeinformationen zum Azure Functions-Projekt
In diesem Abschnitt implementieren Sie den Clientanmeldeinformationsfluss im Azure Functions-Projekt, um ein Zugriffstoken zu erhalten, das mit Microsoft Graph kompatibel ist.
Öffnen Sie Ihre CLI in dem Verzeichnis, das GraphTutorial.csproj enthält.
Fügen Sie ihre Webhook-Anwendungs-ID und ihren geheimen Schlüssel mit den folgenden Befehlen zum geheimen Speicher hinzu. Ersetzen Sie
YOUR_WEBHOOK_APP_ID_HERE
dies durch die Anwendungs-ID für den Graph Azure Function Webhook. Ersetzen SieYOUR_WEBHOOK_APP_SECRET_HERE
dies durch den Anwendungsschlüssel, den Sie im Azure-Portal für den Graph Azure Function Webhook erstellt haben.dotnet user-secrets set webHookId "YOUR_WEBHOOK_APP_ID_HERE" dotnet user-secrets set webHookSecret "YOUR_WEBHOOK_APP_SECRET_HERE"
Erstellen eines Authentifizierungsanbieters für Clientanmeldeinformationen
Erstellen Sie eine neue Datei im Verzeichnis "./GraphTutorial/Authentication" mit dem Namen "ClientCredentialsAuthProvider.cs", und fügen Sie den folgenden Code hinzu.
using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Identity.Client; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public class ClientCredentialsAuthProvider : IAuthenticationProvider { private IConfidentialClientApplication _msalClient; private string[] _scopes; private ILogger _logger; public ClientCredentialsAuthProvider( string appId, string clientSecret, string tenantId, string[] scopes, ILogger logger) { _scopes = scopes; _logger = logger; _msalClient = ConfidentialClientApplicationBuilder .Create(appId) .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true) .WithTenantId(tenantId) .WithClientSecret(clientSecret) .Build(); } public async Task<string> GetAccessToken() { try { // Invoke client credentials flow // NOTE: This will return a cached token if a valid one // exists var result = await _msalClient .AcquireTokenForClient(_scopes) .ExecuteAsync(); _logger.LogInformation($"App-only access token: {result.AccessToken}"); return result.AccessToken; } catch (Exception exception) { _logger.LogError(exception, "Error getting access token via client credentials flow"); return null; } } // This is the delegate called by the GraphServiceClient on each // request. public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage) { // Get the current access token var token = await GetAccessToken(); // Add the token in the Authorization header requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } } }
Nehmen Sie sich einen Moment Zeit, um zu überlegen, was der Code in "ClientCredentialsAuthProvider.cs " bewirkt.
- Im Konstruktor wird eine ConfidentialClientApplication aus dem
Microsoft.Identity.Client
Paket initialisiert. Es verwendet dieWithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
und.WithTenantId(tenantId)
Funktionen, um die Anmeldegruppe auf die angegebene Microsoft 365 Organisation zu beschränken. - In der
GetAccessToken
Funktion wird aufgerufenAcquireTokenForClient
, um ein Token für die Anwendung abzurufen. Der Tokenfluss der Clientanmeldeinformationen ist immer nicht interaktiv. - Sie implementiert die
Microsoft.Graph.IAuthenticationProvider
Schnittstelle, sodass diese Klasse im Konstruktor derGraphServiceClient
zum Authentifizieren ausgehender Anforderungen übergeben werden kann.
Aktualisieren von GraphClientService
Öffnen Sie GraphClientService.cs , und fügen Sie der Klasse die folgende Eigenschaft hinzu.
private GraphServiceClient _appGraphClient;
Ersetzen Sie die vorhandene
GetAppGraphClient
-Funktion durch Folgendes.public GraphServiceClient GetAppGraphClient(ILogger logger) { if (_appGraphClient == null) { // Create a client credentials auth provider var authProvider = new ClientCredentialsAuthProvider( _config["webHookId"], _config["webHookSecret"], _config["tenantId"], // The https://graph.microsoft.com/.default scope // is required for client credentials. It requests // all of the permissions that are explicitly set on // the app registration new[] { "https://graph.microsoft.com/.default" }, logger); _appGraphClient = new GraphServiceClient(authProvider); } return _appGraphClient; }
Implementieren der Funktion "Benachrichtigen"
In diesem Abschnitt implementieren Sie die Notify
Funktion, die als Benachrichtigungs-URL für Änderungsbenachrichtigungen verwendet wird.
Erstellen Sie ein neues Verzeichnis im GraphTutorials-Verzeichnis namens "Models".
Erstellen Sie eine neue Datei im Verzeichnis "Models" mit dem Namen "ResourceData.cs", und fügen Sie den folgenden Code hinzu.
namespace GraphTutorial.Models { // Class to represent the resourceData object // inside a change notification public class ResourceData { public string Id { get;set; } } }
Erstellen Sie eine neue Datei im Verzeichnis "Models" mit dem Namen "ChangeNotificationPayload.cs" , und fügen Sie den folgenden Code hinzu.
Erstellen Sie eine neue Datei im Verzeichnis "Models" mit dem Namen "NotificationList.cs" , und fügen Sie den folgenden Code hinzu.
namespace GraphTutorial.Models { // Class representing an array of notifications // in a notification payload public class NotificationList { public ChangeNotification[] Value { get;set; } } }
Öffnen Sie ./GraphTutorial/Notify.cs , und ersetzen Sie den gesamten Inhalt durch Folgendes.
using GraphTutorial.Models; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Graph; using System.IO; using System.Text.Json; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class Notify { public static readonly string ClientState = "GraphTutorialState"; private IConfiguration _config; private IGraphClientService _clientService; public Notify(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("Notify")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["webHookId"]) || string.IsNullOrEmpty(_config["webHookSecret"]) || string.IsNullOrEmpty(_config["tenantId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } // Is this a validation request? // https://docs.microsoft.com/graph/webhooks#notification-endpoint-validation string validationToken = req.Query["validationToken"]; if (!string.IsNullOrEmpty(validationToken)) { // Because validationToken is a string, OkObjectResult // will return a text/plain response body, which is // required for validation return new OkObjectResult(validationToken); } // Not a validation request, process the body var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); log.LogInformation($"Change notification payload: {requestBody}"); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; // Deserialize the JSON payload into a list of ChangeNotification // objects var notifications = JsonSerializer.Deserialize<NotificationList>(requestBody, jsonOptions); foreach (var notification in notifications.Value) { if (notification.ClientState == ClientState) { // Process each notification await ProcessNotification(notification, log); } else { log.LogInformation($"Notification received with unexpected client state: {notification.ClientState}"); } } // Return 202 per docs return new AcceptedResult(); } private async Task ProcessNotification(ChangeNotification notification, ILogger log) { var graphClient = _clientService.GetAppGraphClient(log); // The resource field in the notification has the URL to the // message, including the user ID and message ID. Since we // have the URL, use a MessageRequestBuilder instead of the fluent // API var msgRequestBuilder = new MessageRequestBuilder( $"https://graph.microsoft.com/v1.0/{notification.Resource}", graphClient); var message = await msgRequestBuilder.Request() .Select(m => new { m.Subject }) .GetAsync(); log.LogInformation($"The following message was {notification.ChangeType}:"); log.LogInformation($"Subject: {message.Subject}, ID: {message.Id}"); } } }
Nehmen Sie sich einen Moment Zeit, um zu überlegen, was der Code in Notify.cs bewirkt.
- Die
Run
Funktion überprüft, ob einvalidationToken
Abfrageparameter vorhanden ist. Wenn dieser Parameter vorhanden ist, verarbeitet er die Anforderung als Validierungsanforderung und antwortet entsprechend. - Wenn es sich bei der Anforderung nicht um eine Überprüfungsanforderung handelt, wird die JSON-Nutzlast in eine
ChangeNotificationCollection
deserialisiert. - Jede Benachrichtigung in der Liste wird auf den erwarteten Clientstatuswert überprüft und verarbeitet.
- Die Nachricht, die die Benachrichtigung ausgelöst hat, wird mit Microsoft Graph abgerufen.
Implementieren der SetSubscription-Funktion
In diesem Abschnitt implementieren Sie die SetSubscription-Funktion. Diese Funktion fungiert als API, die von der Testanwendung aufgerufen wird, um ein Abonnement im Posteingang eines Benutzers zu erstellen oder zu löschen.
Erstellen Sie eine neue Datei im Verzeichnis "Models " mit dem Namen "SetSubscriptionPayload.cs" , und fügen Sie den folgenden Code hinzu.
namespace GraphTutorial.Models { // Class to represent the payload sent to the // SetSubscription function public class SetSubscriptionPayload { // "subscribe" or "unsubscribe" public string RequestType { get;set; } // If unsubscribing, the subscription to delete public string SubscriptionId { get;set; } // If subscribing, the user ID to subscribe to // Can be object ID of user, or userPrincipalName public string UserId { get;set; } } }
Öffnen Sie ./GraphTutorial/SetSubscription.cs , und ersetzen Sie den gesamten Inhalt durch Folgendes.
using GraphTutorial.Authentication; using GraphTutorial.Models; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Graph; using System; using System.IO; using System.Text.Json; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class SetSubscription { private IConfiguration _config; private IGraphClientService _clientService; public SetSubscription(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("SetSubscription")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["webHookId"]) || string.IsNullOrEmpty(_config["webHookSecret"]) || string.IsNullOrEmpty(_config["tenantId"]) || string.IsNullOrEmpty(_config["apiFunctionId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } var notificationHost = _config["ngrokUrl"]; if (string.IsNullOrEmpty(notificationHost)) { notificationHost = req.Host.Value; } // Validate the bearer token var validationResult = await TokenValidation.ValidateAuthorizationHeader( req, _config["tenantId"], _config["apiFunctionId"], log); // If token wasn't returned it isn't valid if (validationResult == null) { return new UnauthorizedResult(); } var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; // Deserialize the JSON payload into a SetSubscriptionPayload object var payload = JsonSerializer.Deserialize<SetSubscriptionPayload>(requestBody, jsonOptions); if (payload == null) { return new BadRequestErrorMessageResult("Invalid request payload"); } // Initialize Graph client var graphClient = _clientService.GetAppGraphClient(log); if (payload.RequestType.ToLower() == "subscribe") { if (string.IsNullOrEmpty(payload.UserId)) { return new BadRequestErrorMessageResult("Required fields in payload missing"); } // Create a new subscription object var subscription = new Subscription { ChangeType = "created,updated", NotificationUrl = $"{notificationHost}/api/Notify", Resource = $"/users/{payload.UserId}/mailfolders/inbox/messages", ExpirationDateTime = DateTimeOffset.UtcNow.AddDays(2), ClientState = Notify.ClientState }; // POST /subscriptions var createdSubscription = await graphClient.Subscriptions .Request() .AddAsync(subscription); return new OkObjectResult(createdSubscription); } else { if (string.IsNullOrEmpty(payload.SubscriptionId)) { return new BadRequestErrorMessageResult("Subscription ID missing in payload"); } // DELETE /subscriptions/subscriptionId await graphClient.Subscriptions[payload.SubscriptionId] .Request() .DeleteAsync(); return new AcceptedResult(); } } } }
Nehmen Sie sich einen Moment Zeit, um zu überlegen, was der Code in "SetSubscription.cs " bewirkt.
- Die
Run
Funktion liest die JSON-Nutzlast, die in der POST-Anforderung gesendet wurde, um den Anforderungstyp (Abonnieren oder Kündigen des Abonnements), die benutzer-ID, die abonniert werden soll, und die Abonnement-ID zum Kündigen des Abonnements zu ermitteln. - Wenn es sich bei der Anforderung um eine Abonnementanforderung handelt, verwendet sie das Microsoft Graph SDK, um ein neues Abonnement im Posteingang des angegebenen Benutzers zu erstellen. Das Abonnement benachrichtigt, wenn Nachrichten erstellt oder aktualisiert werden. Das neue Abonnement wird in der JSON-Nutzlast der Antwort zurückgegeben.
- Wenn es sich bei der Anforderung um eine Anforderung zum Kündigen des Abonnements handelt, verwendet sie das Microsoft Graph SDK, um das angegebene Abonnement zu löschen.
Aufrufen von "SetSubscription" aus der Test-App
In diesem Abschnitt implementieren Sie Funktionen zum Erstellen und Löschen von Abonnements in der Test-App.
Öffnen Sie ./TestClient/azurefunctions.js , und fügen Sie die folgende Funktion hinzu.
async function createSubscription() { // Get the user to subscribe for const userId = document.getElementById('subscribe-user').value; if (!userId) { updatePage(Views.error, { message: 'Please provide a user ID or userPrincipalName' }); return; } const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } // Build the JSON payload for the subscribe request const payload = { requestType: 'subscribe', userId: userId }; const response = await fetch('http://localhost:7071/api/SetSubscription', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: JSON.stringify(payload) }); if (response.ok) { // Get the new subscription from the response const subscription = await response.json(); // Add the new subscription to the array of subscriptions // in the session let existingSubscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')) || []; existingSubscriptions.push({ userId: userId, subscriptionId: subscription.id }); sessionStorage.setItem('graph-subscriptions', JSON.stringify(existingSubscriptions)); // Refresh the subscriptions page to display the new // subscription updatePage(Views.subscriptions); return; } updatePage(Views.error, { message: `Call to SetSubscription returned ${response.status}`, debug: response.statusText }); }
Dieser Code ruft die
SetSubscription
Azure-Funktion zum Abonnieren auf und fügt das neue Abonnement dem Array von Abonnements in der Sitzung hinzu.Fügen Sie die folgende Funktion zu azurefunctions.js hinzu.
async function deleteSubscription(subscriptionId) { const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } // Build the JSON payload for the unsubscribe request const payload = { requestType: 'unsubscribe', subscriptionId: subscriptionId }; const response = await fetch('http://localhost:7071/api/SetSubscription', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: JSON.stringify(payload) }); if (response.ok) { // Remove the subscription from the array let existingSubscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')) || []; const subscriptionIndex = existingSubscriptions.findIndex((item) => { return item.subscriptionId === subscriptionId; }); existingSubscriptions.splice(subscriptionIndex, 1); sessionStorage.setItem('graph-subscriptions', JSON.stringify(existingSubscriptions)); // Refresh the subscriptions page updatePage(Views.subscriptions); return; } updatePage(Views.error, { message: `Call to SetSubscription returned ${response.status}`, debug: response.statusText }); }
Dieser Code ruft die
SetSubscription
Azure-Funktion auf, um sich vom Abonnement abzumelden, und entfernt das Abonnement aus dem Array der Abonnements in der Sitzung.Wenn ngrok nicht ausgeführt wird, führen Sie ngrok (
ngrok http 7071
) aus, und kopieren Sie die HTTPS-Weiterleitungs-URL.Fügen Sie die ngrok-URL zum geheimen Benutzerspeicher hinzu, indem Sie den folgenden Befehl ausführen.
dotnet user-secrets set ngrokUrl "YOUR_NGROK_URL_HERE"
Wichtig
Wenn Sie ngrok neu starten, müssen Sie diesen Befehl wiederholen, um Ihre ngrok-URL zu aktualisieren.
Ändern Sie das aktuelle Verzeichnis in Ihrer CLI in das Verzeichnis ./GraphTutorial , und führen Sie den folgenden Befehl aus, um die Azure-Funktion lokal zu starten.
func start
Aktualisieren Sie die SPA, und wählen Sie das Abonnement-Navigationselement aus. Geben Sie eine Benutzer-ID für einen Benutzer in Ihrer Microsoft 365 Organisation ein, der über ein Exchange Online Postfach verfügt. Dies
id
kann entweder der Benutzer (von Microsoft Graph) oder das desuserPrincipalName
Benutzers sein. Klicken Sie auf "Abonnieren".Die Seite wird aktualisiert, auf der das neue Abonnement in der Tabelle angezeigt wird.
Senden Sie eine E-Mail an den Benutzer. Nach kurzer Zeit sollte die
Notify
Funktion aufgerufen werden. Sie können dies in der ngrok-Webschnittstelle (http://localhost:4040
) oder in der Debugausgabe des Azure Function-Projekts überprüfen.... [7/8/2020 7:33:57 PM] The following message was created: [7/8/2020 7:33:57 PM] Subject: Hi Megan!, ID: AAMkAGUyN2I4N2RlLTEzMTAtNDBmYy1hODdlLTY2NTQwODE2MGEwZgBGAAAAAAA2J9QH-DvMRK3pBt_8rA6nBwCuPIFjbMEkToHcVnQirM5qAAAAAAEMAACuPIFjbMEkToHcVnQirM5qAACHmpAsAAA= [7/8/2020 7:33:57 PM] Executed 'Notify' (Succeeded, Id=9c40af0b-e082-4418-aa3a-aee624f30e7a) ...
Klicken Sie in der Test-App in der Tabellenzeile für das Abonnement auf "Löschen ". Die Seite wird aktualisiert, und das Abonnement ist nicht mehr in der Tabelle enthalten.
Vorbereiten der Veröffentlichung in Azure
In dieser Übung erfahren Sie, welche Änderungen an der Beispiel-Azure-Funktion erforderlich sind, um sich auf die Veröffentlichung in einer Azure Functions-App vorzubereiten.
Aktualisieren des Codes
Die Konfiguration wird aus dem geheimen Benutzerspeicher gelesen, was nur für Ihren Entwicklungscomputer gilt. Vor der Veröffentlichung in Azure müssen Sie den Speicherort Ihrer Konfiguration ändern und den Code in "Program.cs" entsprechend aktualisieren.
Anwendungsgeheimnisse sollten in sicherem Speicher gespeichert werden, z. B. Azure Key Vault.
Aktualisieren der CORS-Einstellung für Azure-Funktion
In diesem Beispiel haben wir CORS in "local.settings.json" so konfiguriert, dass die Testanwendung die Funktion aufrufen kann. Sie müssen Ihre veröffentlichte Funktion so konfigurieren, dass alle SPA-Apps zugelassen werden, die sie aufrufen.
Aktualisieren von App-Registrierungen
Die knownClientApplications
Eigenschaft im Manifest für die Graph Azure Function-App-Registrierung muss mit den Anwendungs-IDs aller Apps aktualisiert werden, die die Azure-Funktion aufrufen.
Neu erstellen vorhandener Abonnements
Alle Abonnements, die mithilfe der Webhook-URL auf Ihrem lokalen Computer oder ngrok erstellt wurden, sollten mithilfe der Produktions-URL der Notify
Azure-Funktion neu erstellt werden.
Herzlichen Glückwunsch!
Sie haben das Lernprogramm "Azure Functions Microsoft Graph" abgeschlossen. Nachdem Sie nun über eine funktionierende App verfügen, die Microsoft Graph aufruft, können Sie experimentieren und neue Features hinzufügen. Besuchen Sie die Übersicht über Microsoft Graph, um alle Daten anzuzeigen, auf die Sie mit Microsoft Graph zugreifen können.
Feedback
Bitte geben Sie Feedback zu diesem Lernprogramm im GitHub Repository.
Liegt ein Problem mit diesem Abschnitt vor? Wenn ja, senden Sie uns Feedback, damit wir den Abschnitt verbessern können.