Créer des fonctions Azure avec Microsoft Graph
Ce didacticiel vous apprend à créer une fonction Azure qui utilise l’API Microsoft Graph pour récupérer des informations de calendrier pour un utilisateur.
Conseil
Si vous préférez simplement télécharger le didacticiel terminé, vous pouvez télécharger ou cloner le GitHub complet. Consultez le fichier README dans le dossier de démonstration pour obtenir des instructions sur la configuration de l’application avec un ID d’application et une secret.
Conditions préalables
Avant de commencer ce didacticiel, vous devez avoir installé les outils suivants sur votre ordinateur de développement.
Vous devez également avoir un compte scolaire ou scolaire Microsoft, avec accès à un compte d’administrateur général dans la même organisation. Si vous n’avez pas de compte Microsoft, vous pouvez vous inscrire au programme Microsoft 365 développeur pour obtenir un abonnement Office 365 gratuit.
Notes
Ce didacticiel a été écrit avec les versions suivantes des outils ci-dessus. Les étapes de ce guide peuvent fonctionner avec d’autres versions, mais elles n’ont pas été testées.
- .NET Core SDK 5.0.203
- Azure Functions Core Tools 3.0.3442
- Azure CLI 2.23.0
- ngrok 2.3.40
Commentaires
N’hésitez pas à nous faire part de vos commentaires sur ce didacticiel dans GitHub référentiel.
Créer un projet Azure Functions
Dans ce didacticiel, vous allez créer une fonction Azure simple qui implémente des fonctions de déclencheur HTTP qui appellent Microsoft Graph. Ces fonctions couvrent les scénarios suivants :
- Implémente une API pour accéder à la boîte de réception d’un utilisateur à l’aide de l’authentification de flux « de la part de ».
- Implémente une API pour s’abonner et se désabonner pour les notifications dans la boîte de réception d’un utilisateur, à l’aide de l’authentification de flux d’octroi d’informations d’identification client.
- Implémente un webhook pour recevoir des notifications de modification de Microsoft Graph et accéder aux données à l’aide du flux d’octroi d’informations d’identification client.
Vous allez également créer une application Mono-page JavaScript simple pour appeler les API implémentées dans la fonction Azure.
Créer un projet Azure Functions
Ouvrez votre interface de ligne de commande (CLI) dans un répertoire où vous souhaitez créer le projet. Exécutez la commande suivante :
func init GraphTutorial --worker-runtime dotnetisolated
Modifiez le répertoire actuel de votre CLI en répertoire GraphTutorial et exécutez les commandes suivantes pour créer trois fonctions dans le projet.
func new --name GetMyNewestMessage --template "HTTP trigger" func new --name SetSubscription --template "HTTP trigger" func new --name Notify --template "HTTP trigger"
Ouvrez local.settings.json et ajoutez ce qui suit au fichier pour autoriser CORS à
http://localhost:8080
partir de , l’URL de l’application de test."Host": { "CORS": "http://localhost:8080" }
Exécutez la commande suivante pour exécuter le projet localement.
func start
Si tout fonctionne, vous verrez le résultat suivant :
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
Vérifiez que les fonctions fonctionnent correctement en ouvrant votre navigateur et en naviguant vers les URL de fonction affichées dans la sortie. Vous devriez voir le message suivant dans votre navigateur :
Welcome to Azure Functions!
.
Créer une application à page unique
Ouvrez votre CLI dans un répertoire où vous souhaitez créer le projet. Créez un répertoire nommé TestClient pour contenir vos fichiers HTML et JavaScript.
Créez un fichier nommé index.html dans le répertoire TestClient et ajoutez le code suivant.
<!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>
Cela définit la disposition de base de l’application, y compris une barre de navigation. Il ajoute également les choses suivantes :
- Bootstrap et son javaScript de prise en charge
- FontAwesome
- Bibliothèque d’authentification Microsoft pour JavaScript (MSAL.js) 2.0
Conseil
La page inclut un (
<link rel="shortcut icon" href="g-raph.png">
). Vous pouvez supprimer cette ligne ou télécharger le fichier g-raph.png à partir de GitHub.Créez un fichier nommé style.css dans le répertoire TestClient et ajoutez le code suivant.
body { padding-top: 70px; }
Créez un fichier nommé ui.js dans le répertoire TestClient et ajoutez le code suivant.
// 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);
Ce code utilise JavaScript pour afficher la page actuelle en fonction de l’affichage sélectionné.
Tester l’application à page unique
Notes
Cette section contient des instructions sur l’utilisation de dotnet-serve pour exécuter un serveur HTTP de test simple sur votre ordinateur de développement. L’utilisation de cet outil spécifique n’est pas requise. Vous pouvez utiliser n’importe quel serveur de test de votre préférence pour servir le répertoire TestClient .
Exécutez la commande suivante dans votre CLI pour installer dotnet-serve.
dotnet tool install --global dotnet-serve
Modifiez le répertoire actuel de votre CLI en répertoire TestClient et exécutez la commande suivante pour démarrer un serveur HTTP.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate" -p 8080
Ouvrez votre navigateur et accédez à
http://localhost:8080
. La page doit s’restituer, mais aucun des boutons ne fonctionne actuellement.
Ajouter des packages NuGet
Avant de passer à la suite, installez des packages NuGet supplémentaires que vous utiliserez ultérieurement.
- Microsoft.Azure.Functions.Extensions pour activer l’injection de dépendances dans le projet Fonctions Azure.
- Microsoft.Extensions.Configuration.UserSecrets pour lire la configuration de l’application à partir du magasin de secrets de développement .NET.
- Microsoft.Graph pour effectuer des appels Microsoft Graph.
- Microsoft.Identity.Client pour l’authentification et la gestion des jetons.
- Microsoft.IdentityModel.Protocols.OpenIdConnect pour récupérer la configuration OpenID pour la validation du jeton.
- System.IdentityModel.Tokens.Jwt pour la validation des jetons envoyés à l’API web.
Modifiez le répertoire actuel de votre CLI en répertoire GraphTutorial et exécutez les commandes suivantes.
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
Inscrire les applications dans le portail
Dans cet exercice, vous allez créer trois applications Azure AD à l’aide du centre Azure Active Directory’administration :
- Une inscription d’application pour l’application à page unique afin qu’elle puisse se connecter aux utilisateurs et obtenir des jetons permettant à l’application d’appeler la fonction Azure.
- Une inscription d’application pour la fonction Azure qui lui permet d’utiliser le flux de la part de pour échanger le jeton envoyé par la SPA contre un jeton qui lui permettra d’appeler Microsoft Graph.
- Une inscription d’application pour le webhook de fonction Azure qui lui permet d’utiliser le flux d’informations d’identification client pour appeler Microsoft Graph sans utilisateur.
Notes
Cet exemple nécessite trois inscriptions d’application, car il implémente à la fois le flux « de la part de » et le flux d’informations d’identification du client. Si votre fonction Azure utilise uniquement l’un de ces flux, vous devez uniquement créer les inscriptions d’application correspondant à ce flux.
Ouvrez un navigateur et accédez au Centre d’administration Azure Active Directory et connectez-vous à l’aide d’Microsoft 365'organisation client.
Sélectionnez Azure Active Directory dans le volet de navigation gauche, puis sélectionnez Inscriptions d’applications sous Gérer.
Inscrire une application pour l’application à page unique
Sélectionnez Nouvelle inscription. Sur la page Inscrire une application, définissez les valeurs comme suit.
- Définissez le Nom sur
Graph Azure Function Test App
. - Définissez les types de comptes pris en charge sur Comptes dans cet annuaire d’organisation uniquement.
- Sous URI de redirection, modifiez la dropdown en application mono-page (SPA) et définissez la valeur sur
http://localhost:8080
.
- Définissez le Nom sur
Sélectionner Inscription. Dans la page Graph Application de test de fonction Azure, copiez les valeurs de l’ID d’application (client) et de l’ID d’annuaire (client) et enregistrez-les. Vous en aurez besoin dans les étapes ultérieures.
Inscrire une application pour la fonction Azure
Revenir aux inscriptions d’application, puis sélectionnez Nouvelle inscription. Sur la page Inscrire une application, définissez les valeurs comme suit.
- Définissez le Nom sur
Graph Azure Function
. - Définissez les types de comptes pris en charge sur Comptes dans cet annuaire d’organisation uniquement.
- Laissez l’URI de redirection vide.
- Définissez le Nom sur
Sélectionnez Inscrire. Dans la page Graph fonction Azure, copiez la valeur de l’ID d’application (client) et enregistrez-la. Vous en aurez besoin à l’étape suivante.
Sélectionnez Certificats et secrets sous Gérer. Sélectionnez le bouton Nouveau secret client. Entrez une valeur dans Description, sélectionnez une des options pour Expire le, puis sélectionnez Ajouter.
Copiez la valeur due la clé secrète client avant de quitter cette page. Vous en aurez besoin à l’étape suivante.
Important
Ce secret client n’apparaîtra plus jamais, aussi veillez à le copier maintenant.
Sélectionnez les autorisations d’API sous Gérer. Sélectionnez Ajouter une autorisation.
Sélectionnez Microsoft Graph, puis Autorisations déléguées. Ajoutez Mail.Read et sélectionnez Ajouter des autorisations.
Sélectionnez Exposer une API sous Gérer, puis ajoutez une étendue.
Acceptez l’URI d’ID d’application par défaut, puis choisissez Enregistrer et continuer.
Remplissez le formulaire Ajouter une étendue comme suit :
- Nom de l’étendue : Mail.Read
- Qui pouvez-vous donner votre consentement ? : Administrateurs et utilisateurs
- Nom complet du consentement de l’administrateur : Lire les boîtes de réception de tous les utilisateurs
- Description du consentement de l’administrateur : Permet à l’application de lire les boîtes de réception de tous les utilisateurs
- Nom complet du consentement de l’utilisateur : Lire votre boîte de réception
- Description du consentement de l’utilisateur : Permet à l’application de lire votre boîte de réception
- État : activé
Sélectionnez Ajouter une étendue.
Copiez la nouvelle étendue, vous en aurez besoin dans les étapes ultérieures.
Sélectionnez Manifeste sous Gérer.
Recherchez
knownClientApplications
dans le manifeste et remplacez sa valeur actuelle par ,TEST_APP_ID
où est l’ID d’application de l’Graph’application[]
[TEST_APP_ID]
Test de fonction Azure. Sélectionnez Enregistrer.
Notes
L’ajout de l’ID knownClientApplications
d’application de test à la propriété dans le manifeste de la fonction Azure permet à l’application test de déclencher un flux de consentement combiné. Cela est nécessaire pour que le flux « de la part de » fonctionne.
Ajouter une étendue de fonction Azure pour tester l’inscription de l’application
Revenir à la Graph’inscription de l’application test de fonction Azure, puis sélectionnez les autorisations d’API sous Gérer. Sélectionnez Ajouter une autorisation.
Sélectionnez Mes API, puis Chargez plus. Sélectionnez Graph Azure Function.
Sélectionnez l’autorisation Mail.Read , puis sélectionnez Ajouter des autorisations.
Dans les autorisations configurées, supprimez l’autorisation User.Read sous Microsoft Graph en sélectionnant ... à droite de l’autorisation et en sélectionnant Supprimer l’autorisation. Sélectionnez Oui, supprimer pour confirmer.
Inscrire une application pour le webhook de fonction Azure
Revenir aux inscriptions d’application, puis sélectionnez Nouvelle inscription. Sur la page Inscrire une application, définissez les valeurs comme suit.
- Définissez le Nom sur
Graph Azure Function Webhook
. - Définissez les types de comptes pris en charge sur Comptes dans cet annuaire d’organisation uniquement.
- Laissez l’URI de redirection vide.
- Définissez le Nom sur
Sélectionnez Inscrire. Sur la page Graph webhook de fonction Azure, copiez la valeur de l’ID d’application (client) et enregistrez-la, vous en aurez besoin à l’étape suivante.
Sélectionnez Certificats et secrets sous Gérer. Sélectionnez le bouton Nouveau secret client. Entrez une valeur dans Description, sélectionnez une des options pour Expire le, puis sélectionnez Ajouter.
Copiez la valeur du secret client avant de quitter cette page. Vous en aurez besoin à l’étape suivante.
Sélectionnez les autorisations d’API sous Gérer. Sélectionnez Ajouter une autorisation.
Sélectionnez Microsoft Graph, puis Autorisations d’application. Ajoutez User.Read.All et Mail.Read, puis sélectionnez Ajouter des autorisations.
Dans les autorisations configurées, supprimez l’autorisation User.Read déléguée sous Microsoft Graph en sélectionnant... à droite de l’autorisation et en sélectionnant Supprimer l’autorisation. Sélectionnez Oui, supprimer pour confirmer.
Sélectionnez le bouton Accorder le consentement de l’administrateur pour... puis sélectionnez Oui pour accorder le consentement administrateur pour les autorisations d’application configurées. La colonne État de la table Autorisations configurées est changée en Accordé pour....
Implémenter l’API avec l’authentification de la part de
Dans cet exercice, vous terminerez l’implémentation de la fonction Azure GetMyNewestMessage
et mettrez à jour le client de test pour appeler la fonction.
La fonction Azure utilise le flux « de la part de ». L’ordre de base des événements dans ce flux est le :
- L’application de test utilise un flux d’th interactive pour permettre à l’utilisateur de se connecter et d’accorder son consentement. Elle obtient un jeton qui est étendue à la fonction Azure. Le jeton ne contient pas d’étendues Graph Microsoft.
- L’application de test appelle la fonction Azure, en envoyant son jeton d’accès dans l’en-tête
Authorization
. - La fonction Azure valide le jeton, puis échange ce jeton contre un deuxième jeton d’accès qui contient les étendues Graph Microsoft.
- La fonction Azure appelle Microsoft Graph au nom de l’utilisateur à l’aide du deuxième jeton d’accès.
Important
Pour éviter de stocker l’ID d’application et la secret dans la source, vous utiliserez le gestionnaire de secret .NET pour stocker ces valeurs. Le Gestionnaire de secret est uniquement à des fins de développement, les applications de production doivent utiliser un gestionnaire de secret approuvé pour stocker les secrets.
Ajouter l’authentification à l’application à page unique
Commencez par ajouter l’authentification à la SPA. Cela permettra à l’application d’obtenir un jeton d’accès accordant l’accès pour appeler la fonction Azure. Comme il s’agit d’une SPA, elle utilise le flux de code d’autorisation avec PKCE.
Créez un fichier dans le répertoire TestClient nommé config.js et ajoutez le code suivant.
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' ] }
Remplacez
YOUR_TEST_APP_APP_ID_HERE
par l’ID d’application que vous avez créé dans le portail Azure pour Graph’application test de fonction Azure. RemplacezYOUR_TENANT_ID_HERE
par la valeur d’ID d’annuaire (client) que vous avez copiée à partir du portail Azure. RemplacezYOUR_AZURE_FUNCTION_APP_ID_HERE
par l’ID d’application pour Graph fonction Azure.Important
Si vous utilisez un contrôle source tel que Git, il est temps d’exclure le fichier config.js du contrôle source afin d’éviter toute fuite accidentelle de vos ID d’application et ID de client.
Créez un fichier dans le répertoire TestClient nommé auth.js et ajoutez le code suivant.
// 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(); }
Prenez en compte ce que fait ce code.
- Il initialise une à l’aide
PublicClientApplication
des valeurs stockées dans config.js. - Il utilise pour
loginPopup
se connecter à l’utilisateur, à l’aide de l’étendue d’autorisation pour la fonction Azure. - Il stocke le nom d’utilisateur de l’utilisateur dans la session.
Important
Étant donné que l’application
loginPopup
utilise , vous devrez peut-être modifier le bloqueur de fenêtres pop-up de votre navigateur pour autoriser les fenêtres pop-up à partir dehttp://localhost:8080
.- Il initialise une à l’aide
Actualisez la page et connectez-vous. La page doit être mise à jour avec le nom d’utilisateur, ce qui indique que la signature a réussi.
Ajouter l’authentification à la fonction Azure
Dans cette section, vous allez implémenter le flux de la part de dans la fonction Azure pour obtenir un jeton d’accès GetMyNewestMessage
compatible avec Microsoft Graph.
Initialisez le magasin de secrets de développement .NET en ouvrant votre CLI dans le répertoire qui contient GraphTutorial.csproj et en exécutant la commande suivante.
dotnet user-secrets init
Ajoutez votre ID d’application, votre secret et votre ID de client au magasin secret à l’aide des commandes suivantes. Remplacez
YOUR_API_FUNCTION_APP_ID_HERE
par l’ID d’application pour Graph fonction Azure. RemplacezYOUR_API_FUNCTION_APP_SECRET_HERE
par la secret d’application que vous avez créée dans le portail Azure pour la Graph Azure Function. RemplacezYOUR_TENANT_ID_HERE
par la valeur d’ID d’annuaire (client) que vous avez copiée à partir du portail Azure.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"
Traiter le jeton du porteur entrant
Dans cette section, vous allez implémenter une classe pour valider et traiter le jeton du porteur envoyé à partir de la SPA vers la fonction Azure.
Créez un répertoire dans le répertoire GraphTutorial nommé Authentication.
Créez un fichier nommé TokenValidationResult.cs dans le dossier ./GraphTutorial/Authentication et ajoutez le code suivant.
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; } } }
Créez un fichier nommé TokenValidation.cs dans le dossier ./GraphTutorial/Authentication et ajoutez le code suivant.
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; } } }
Prenez en compte ce que fait ce code.
- Il s’assure qu’il existe un jeton du porteur dans l’en-tête
Authorization
. - Il vérifie la signature et l’émetteur de la configuration OpenID publiée d’Azure.
- Il vérifie que l’audience (
aud
revendication) correspond à l’ID d’application de la fonction Azure. - Il pare le jeton et génère un ID de compte MSAL, qui sera nécessaire pour tirer parti de la mise en cache du jeton.
Créer un fournisseur d’authentification de la part de
Créez un fichier dans le répertoire d’authentification nommé OnBehalfOfAuthProvider.cs et ajoutez le code suivant à ce fichier.
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); } } }
Prenez le temps de réfléchir à ce que fait le code dans OnBehalfOfAuthProvider.cs .
- Dans la fonction
GetAccessToken
, il tente d’abord d’obtenir un jeton utilisateur à partir du cache de jetons à l’aide deAcquireTokenSilent
. En cas d’échec, il utilise le jeton du porteur envoyé par l’application de test à la fonction Azure pour générer une assertion utilisateur. Il utilise ensuite cette assertion utilisateur pour obtenir un jeton compatible Graph à l’aide deAcquireTokenOnBehalfOf
. - Il implémente l’interface
Microsoft.Graph.IAuthenticationProvider
, ce qui permet à cette classe d’être passée dans le constructeurGraphServiceClient
du constructeur pour authentifier les demandes sortantes.
Implémenter Graph service client
Dans cette section, vous allez implémenter un service qui peut être inscrit pour l’injection de dépendances. Le service sera utilisé pour obtenir un client Graph authentifié.
Créez un répertoire dans le répertoire GraphTutorial nommé Services.
Créez un fichier dans le répertoire Services nommé IGraphClientService.cs et ajoutez le code suivant à ce fichier.
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); } }
Créez un fichier dans le répertoire Services nommé GraphClientService.cs et ajoutez le code suivant à ce fichier.
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 { } }
Ajoutez les propriétés suivantes à la
GraphClientService
classe.// 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;
Ajoutez les fonctions suivantes à la
GraphClientService
classe.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); }
Ajoutez une implémentation d’espace réservé pour la
GetAppGraphClient
fonction. Vous l’implémenterez dans les sections ultérieures.public GraphServiceClient GetAppGraphClient() { throw new System.NotImplementedException(); }
La
GetUserGraphClient
fonction prend les résultats de la validation du jeton et crée une fonction authentifiéeGraphServiceClient
pour l’utilisateur.Ouvrez ./GraphTutorial/Program.cs et remplacez son contenu par ce qui suit.
Ce code ajoute des secrets utilisateur à la configuration et active l’injection de dépendances dans vos fonctions Azure, exposant le
GraphClientService
service.
Implémenter la fonction GetMyNewestMessage
Ouvrez ./GraphTutorial/GetMyNewestMessage.cs et remplacez tout son contenu par ce qui suit.
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]); } } }
Passer en revue le code dans GetMyNewestMessage.cs
Prenez le temps de réfléchir à ce que fait le code dans GetMyNewestMessage.cs .
- Dans le constructeur, il enregistre les
IConfiguration
IGraphClientService
objets transmis via l’injection de dépendance. - Dans la
Run
fonction, elle fait les choses suivantes :- Valide que les valeurs de configuration requises sont présentes dans l’objet
IConfiguration
. - Valide le jeton du porteur et renvoie un
401
code d’état si le jeton n’est pas valide. - Obtient un client Graph de l’utilisateur
GraphClientService
qui a effectué cette demande. - Utilise le SDK Microsoft Graph pour obtenir le dernier message de la boîte de réception de l’utilisateur et le renvoie sous forme de corps JSON dans la réponse.
- Valide que les valeurs de configuration requises sont présentes dans l’objet
Appeler la fonction Azure à partir de l’application de test
Ouvrezauth.js et ajoutez la fonction suivante pour obtenir un jeton d’accès.
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; } } }
Prenez en compte ce que fait ce code.
- Il tente d’abord d’obtenir un jeton d’accès en mode silencieux, sans intervention de l’utilisateur. Étant donné que l’utilisateur doit déjà être signé, MSAL doit avoir des jetons pour l’utilisateur dans son cache.
- En cas d’échec avec une erreur qui indique que l’utilisateur doit interagir, il tente d’obtenir un jeton de manière interactive.
Conseil
https://jwt.ms
aud
Vous pouvez consulter le jeton d’accès et vérifier que la revendication est l’ID d’application pour la fonction Azure etscp
que la revendication contient l’étendue d’autorisation de la fonction Azure, et non Microsoft Graph.Créez un fichier dans le répertoire TestClient nommé azurefunctions.js et ajoutez le code suivant.
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 }); } }
Modifiez le répertoire actuel de votre CLI en répertoire ./GraphTutorial et exécutez la commande suivante pour démarrer la fonction Azure localement.
func start
Si la SPA n’est pas déjà en service, ouvrez une deuxième fenêtre CLI et modifiez le répertoire actuel en répertoire ./TestClient . Exécutez la commande suivante pour exécuter l’application de test.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate"
Ouvrez votre navigateur et accédez à
http://localhost:8080
. Connectez-vous et sélectionnez l’élément de navigation Dernier message. L’application affiche des informations sur le dernier message dans la boîte de réception de l’utilisateur.
Implémenter le webhook avec l’authentification des informations d’identification client
Dans cet exercice, vous terminerez l’implémentation des fonctions Azure SetSubscription
Notify
et mettez à jour l’application de test pour vous abonner et vous désabonner aux modifications dans la boîte de réception d’un utilisateur.
- La
SetSubscription
fonction agit en tant qu’API, ce qui permet à l’application de test de créer ou de supprimer un abonnement aux modifications apportées dans la boîte de réception d’un utilisateur. - La
Notify
fonction agit en tant que webhook qui reçoit les notifications de modification générées par l’abonnement.
Les deux fonctions utiliseront le flux d’octroi d’informations d’identification client pour obtenir un jeton d’application uniquement pour appeler Microsoft Graph. Étant donné qu’un administrateur a accordé le consentement administrateur aux étendues d’autorisation requises, aucune interaction utilisateur n’est requise pour obtenir le jeton.
Ajouter l’authentification des informations d’identification client au projet Fonctions Azure
Dans cette section, vous allez implémenter le flux d’informations d’identification client dans le projet Fonctions Azure pour obtenir un jeton d’accès compatible avec Microsoft Graph.
Ouvrez votre CLI dans le répertoire qui contient GraphTutorial.csproj.
Ajoutez votre ID d’application webhook et votre secret au magasin de secrets à l’aide des commandes suivantes. Remplacez-le
YOUR_WEBHOOK_APP_ID_HERE
par l’ID d’application Graph webhook de fonction Azure. RemplacezYOUR_WEBHOOK_APP_SECRET_HERE
par la secret d’application que vous avez créée dans le portail Azure pour Graph Webhook de fonction Azure.dotnet user-secrets set webHookId "YOUR_WEBHOOK_APP_ID_HERE" dotnet user-secrets set webHookSecret "YOUR_WEBHOOK_APP_SECRET_HERE"
Créer un fournisseur d’authentification d’informations d’identification client
Créez un fichier dans le répertoire ./GraphTutorial/Authentication nommé ClientCredentialsAuthProvider.cs et ajoutez le code suivant.
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); } } }
Prenez le temps de réfléchir à ce que fait le code dans ClientCredentialsAuthProvider.cs .
- Dans le constructeur, il initialise un ConfidentialClientApplication à partir du
Microsoft.Identity.Client
package. Il utilise les fonctionsWithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
et les.WithTenantId(tenantId)
fonctions pour limiter l’audience de connexion uniquement à l’Microsoft 365 organisation. - Dans la fonction
GetAccessToken
, il appelle pourAcquireTokenForClient
obtenir un jeton pour l’application. Le flux de jeton d’informations d’identification client est toujours non interactif. - Il implémente l’interface
Microsoft.Graph.IAuthenticationProvider
, ce qui permet à cette classe d’être passée dans le constructeurGraphServiceClient
du constructeur pour authentifier les demandes sortantes.
Mettre à jour GraphClientService
Ouvrez GraphClientService.cs et ajoutez la propriété suivante à la classe.
private GraphServiceClient _appGraphClient;
Remplacez la fonction
GetAppGraphClient
existante par ce qui suit.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; }
Implémenter la fonction Notify
Dans cette section, vous allez implémenter la Notify
fonction, qui sera utilisée comme URL de notification pour les notifications de modification.
Créez un répertoire dans le répertoire GraphTutorials nommé Models.
Créez un fichier dans le répertoire Models nommé ResourceData.cs et ajoutez le code suivant.
namespace GraphTutorial.Models { // Class to represent the resourceData object // inside a change notification public class ResourceData { public string Id { get;set; } } }
Créez un fichier dans le répertoire Models nommé ChangeNotificationPayload.cs et ajoutez le code suivant.
Créez un fichier dans le répertoire Models nommé NotificationList.cs et ajoutez le code suivant.
namespace GraphTutorial.Models { // Class representing an array of notifications // in a notification payload public class NotificationList { public ChangeNotification[] Value { get;set; } } }
Ouvrez ./GraphTutorial/Notify.cs et remplacez tout son contenu par ce qui suit.
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}"); } } }
Prenez le temps de réfléchir à ce que fait le code dans Notify.cs .
- La
Run
fonction vérifie la présence d’un paramètrevalidationToken
de requête. Si ce paramètre est présent, il traite la demande en tant que demande de validation et répond en conséquence. - Si la demande n’est pas une demande de validation, la charge utile JSON est désérialisée en une
ChangeNotificationCollection
. - Chaque notification de la liste est vérifiée pour la valeur d’état client attendue et traitée.
- Le message ayant déclenché la notification est récupéré avec Microsoft Graph.
Implémenter la fonction SetSubscription
Dans cette section, vous allez implémenter la fonction SetSubscription. Cette fonction agit comme une API appelée par l’application de test pour créer ou supprimer un abonnement dans la boîte de réception d’un utilisateur.
Créez un fichier dans le répertoire Models nommé SetSubscriptionPayload.cs et ajoutez le code suivant.
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; } } }
Ouvrez ./GraphTutorial/SetSubscription.cs et remplacez tout son contenu par ce qui suit.
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(); } } } }
Prenez le temps de réfléchir à ce que fait le code dans SetSubscription.cs .
Run
La fonction lit la charge utile JSON envoyée dans la requête POST pour déterminer le type de demande (s’abonner ou se désabonner), l’ID d’utilisateur pour s’abonner et l’ID d’abonnement à désabonner.- Si la demande est une demande d’abonnement, elle utilise le SDK Microsoft Graph pour créer un abonnement dans la boîte de réception de l’utilisateur spécifié. L’abonnement notifiera la création ou la mise à jour des messages. Le nouvel abonnement est renvoyé dans la charge utile JSON de la réponse.
- Si la demande est une demande de désabonnement, elle utilise le SDK Microsoft Graph pour supprimer l’abonnement spécifié.
Appeler SetSubscription à partir de l’application de test
Dans cette section, vous allez implémenter des fonctions pour créer et supprimer des abonnements dans l’application de test.
Ouvrez ./TestClient/azurefunctions.js et ajoutez la fonction suivante.
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 }); }
Ce code appelle la
SetSubscription
fonction Azure pour s’abonner et ajoute le nouvel abonnement au tableau des abonnements dans la session.Ajoutez la fonction suivante à azurefunctions.js.
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 }); }
Ce code appelle la
SetSubscription
fonction Azure pour se désabonner et supprime l’abonnement du tableau des abonnements dans la session.Si ngrok n’est pas en cours d’exécution, exécutez ngrok (
ngrok http 7071
) et copiez l’URL de forwarding HTTPS.Ajoutez l’URL ngrok au magasin de secrets utilisateur en exécutant la commande suivante.
dotnet user-secrets set ngrokUrl "YOUR_NGROK_URL_HERE"
Important
Si vous redémarrez ngrok, vous devrez répéter cette commande pour mettre à jour votre URL ngrok.
Modifiez le répertoire actuel de votre CLI en répertoire ./GraphTutorial et exécutez la commande suivante pour démarrer la fonction Azure localement.
func start
Actualisez la SPA et sélectionnez l’élément de navigation Abonnements . Entrez un ID d’utilisateur pour un utilisateur de votre organisation Microsoft 365 qui dispose d’une boîte aux lettres Exchange Online’utilisateur. Il peut s’agit de l’utilisateur
id
(de Microsoft Graph) ou de l’utilisateuruserPrincipalName
. Cliquez sur S’abonner.La page s’actuale en affichant le nouvel abonnement dans le tableau.
Envoyez un courrier électronique à l’utilisateur. Après un court instant, la
Notify
fonction doit être appelée. Vous pouvez le vérifier dans l’interface web ngrok (http://localhost:4040
) ou dans la sortie de débogage du projet de fonction Azure.... [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) ...
Dans l’application de test, cliquez sur Supprimer dans la ligne de tableau de l’abonnement. La page est actualisée et l’abonnement n’est plus dans le tableau.
Préparer la publication sur Azure
Dans cet exercice, vous allez découvrir les modifications nécessaires à l’exemple de fonction Azure pour préparer la publication dans une application Fonctions Azure.
Mise à jour du code
La configuration est lue à partir du magasin de secret utilisateur, qui s’applique uniquement à votre ordinateur de développement. Avant de publier sur Azure, vous devez modifier l’endroit où vous stockez votre configuration et mettre à jour le code dans Program.cs en conséquence.
Les clés secrètes d’application doivent être stockées dans un stockage sécurisé, tel qu’Azure Key Vault.
Mettre à jour le paramètre CORS pour azure Function
Dans cet exemple, nous avons configuré CORS dans local.settings.json pour permettre à l’application de test d’appeler la fonction. Vous devez configurer votre fonction publiée pour autoriser les applications SPA qui l’appelleront.
Mettre à jour les inscriptions d’applications
La knownClientApplications
propriété dans le manifeste pour l’inscription de l’application de fonction Azure Graph doit être mise à jour avec les ID d’application de toutes les applications qui appelleront la fonction Azure.
Recréer des abonnements existants
Tous les abonnements créés à l’aide de l’URL de webhook sur votre ordinateur local ou ngrok doivent être recréés à l’aide de l’URL de production de la Notify
fonction Azure.
Félicitations !
Vous avez terminé le didacticiel sur les fonctions Microsoft Graph Azure. Maintenant que vous disposez d’une application de travail qui appelle Microsoft Graph, vous pouvez expérimenter et ajouter de nouvelles fonctionnalités. Consultez la vue d’ensemble de Microsoft Graph pour voir toutes les données accessibles avec Microsoft Graph.
Commentaires
N’hésitez pas à nous faire part de vos commentaires sur ce didacticiel dans GitHub référentiel.
Vous avez un problème avec cette section ? Si c'est le cas, faites-nous part de vos commentaires pour que nous puissions l'améliorer.