Crear Funciones de Azure con Microsoft Graph
Este tutorial le enseña a crear una función de Azure que use la API de Microsoft Graph para recuperar información de calendario para un usuario.
Sugerencia
Si prefiere descargar el tutorial completado, puede descargar o clonar el repositorio GitHub archivo. Consulta el archivo README en la carpeta de demostración para obtener instrucciones sobre cómo configurar la aplicación con un identificador de aplicación y un secreto.
Requisitos previos
Antes de iniciar este tutorial, debe tener instaladas las siguientes herramientas en el equipo de desarrollo.
También debe tener una cuenta de trabajo o escuela de Microsoft, con acceso a una cuenta de administrador global en la misma organización. Si no tiene una cuenta de Microsoft, puede registrarse en el programa de desarrolladores de Microsoft 365 para obtener una suscripción Office 365 gratuita.
Nota
Este tutorial se escribió con las siguientes versiones de las herramientas anteriores. Los pasos de esta guía pueden funcionar con otras versiones, pero eso no se ha probado.
- SDK de .NET Core 5.0.203
- Azure Functions Core Tools 3.0.3442
- CLI de Azure 2.23.0
- ngrok 2.3.40
Comentarios
Proporcione cualquier comentario sobre este tutorial en el repositorio GitHub usuario.
Crear un proyecto de Azure Functions
En este tutorial, creará una función sencilla de Azure que implemente funciones de desencadenador HTTP que llamen a Microsoft Graph. Estas funciones cubrirán los siguientes escenarios:
- Implementa una API para obtener acceso a la bandeja de entrada de un usuario mediante la autenticación de flujo en nombre de.
- Implementa una API para suscribirse y cancelar la suscripción a las notificaciones en la bandeja de entrada de un usuario, mediante la autenticación de flujo de concesión de credenciales de cliente.
- Implementa un webhook para recibir notificaciones de cambios de Microsoft Graph obtener acceso a los datos mediante el flujo de concesión de credenciales de cliente.
También creará una aplicación de página única (SPA) de JavaScript sencilla para llamar a las API implementadas en la función de Azure.
Crear proyecto de Azure Functions
Abra la interfaz de línea de comandos (CLI) en un directorio donde desee crear el proyecto. Ejecuta el siguiente comando.
func init GraphTutorial --worker-runtime dotnetisolated
Cambie el directorio actual de la CLI al directorio GraphTutorial y ejecute los siguientes comandos para crear tres funciones en el proyecto.
func new --name GetMyNewestMessage --template "HTTP trigger" func new --name SetSubscription --template "HTTP trigger" func new --name Notify --template "HTTP trigger"
Abra local.settings.json y agregue lo siguiente al archivo para permitir CORS desde
http://localhost:8080
, la dirección URL de la aplicación de prueba."Host": { "CORS": "http://localhost:8080" }
Ejecute el siguiente comando para ejecutar el proyecto localmente.
func start
Si todo funciona, verá el siguiente resultado:
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
Compruebe que las funciones funcionan correctamente abriendo el explorador y explorando las direcciones URL de función que se muestran en el resultado. Debería ver el siguiente mensaje en el explorador:
Welcome to Azure Functions!
.
Crear aplicación de una sola página
Abra la CLI en un directorio donde desee crear el proyecto. Cree un directorio denominado TestClient para contener los archivos HTML y JavaScript.
Cree un nuevo archivo denominado index.html en el directorio TestClient y agregue el siguiente código.
<!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>
Esto define el diseño básico de la aplicación, incluida una barra de navegación. También agrega lo siguiente:
- Bootstrap y su JavaScript compatible
- FontAwesome
- Biblioteca de autenticación de Microsoft para JavaScript (MSAL.js) 2.0
Sugerencia
La página incluye un favicon, (
<link rel="shortcut icon" href="g-raph.png">
). Puede quitar esta línea o puede descargar el archivo g-raph.png desde GitHub.Cree un nuevo archivo denominado style.css en el directorio TestClient y agregue el siguiente código.
body { padding-top: 70px; }
Cree un nuevo archivo denominado ui.js en el directorio TestClient y agregue el siguiente código.
// 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);
Este código usa JavaScript para representar la página actual en función de la vista seleccionada.
Probar la aplicación de una página
Nota
En esta sección se incluyen instrucciones para usar dotnet-serve para ejecutar un servidor HTTP de prueba simple en el equipo de desarrollo. No es necesario usar esta herramienta específica. Puede usar cualquier servidor de prueba que prefiera para atender el directorio TestClient .
Ejecute el siguiente comando en la CLI para instalar dotnet-serve.
dotnet tool install --global dotnet-serve
Cambie el directorio actual de la CLI al directorio TestClient y ejecute el siguiente comando para iniciar un servidor HTTP.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate" -p 8080
Abra el explorador y vaya a
http://localhost:8080
. La página debe representarse, pero ninguno de los botones funciona actualmente.
Agregar paquetes NuGet
Antes de seguir, instala algunos paquetes NuGet que usarás más adelante.
- Microsoft.Azure.Functions.Extensions para habilitar la inserción de dependencias en el proyecto de Azure Functions.
- Microsoft.Extensions.Configuration.UserSecrets para leer la configuración de la aplicación desde el almacén secreto de desarrollo de .NET.
- Microsoft.Graph para realizar llamadas a Microsoft Graph.
- Microsoft.Identity.Client para autenticar y administrar tokens.
- Microsoft.IdentityModel.Protocols.OpenIdConnect para recuperar la configuración de OpenID para la validación de tokens.
- System.IdentityModel.Tokens.Jwt para validar tokens enviados a la API web.
Cambie el directorio actual de la CLI al directorio GraphTutorial y ejecute los siguientes comandos.
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
Registrar las aplicaciones en el portal
En este ejercicio, creará tres nuevas aplicaciones Azure AD con el centro Azure Active Directory administración:
- Un registro de aplicación para la aplicación de una sola página para que pueda iniciar sesión en los usuarios y obtener tokens que permitan a la aplicación llamar a la función de Azure.
- Un registro de aplicación para la función de Azure que le permite usar el flujo en nombre del usuario para intercambiar el token enviado por el SPA por un token que le permitirá llamar a Microsoft Graph.
- Un registro de aplicación para el webhook de función de Azure que le permite usar el flujo de credenciales de cliente para llamar a Microsoft Graph sin un usuario.
Nota
Este ejemplo requiere tres registros de aplicaciones porque está implementando tanto el flujo en nombre del usuario como el flujo de credenciales de cliente. Si la función de Azure solo usa uno de estos flujos, solo tendría que crear los registros de aplicaciones que correspondan a ese flujo.
Abra un explorador y vaya al centro Azure Active Directory de administración e inicie sesión con un administrador Microsoft 365 de la organización de inquilinos.
Seleccione Azure Active Directory en el panel de navegación izquierdo y, a continuación, seleccione Registros de aplicaciones en Administrar.
Registrar una aplicación para la aplicación de una sola página
Seleccione Nuevo registro. En la página Registrar una aplicación, establezca los valores siguientes.
- Establezca Nombre como
Graph Azure Function Test App
. - Establezca los tipos de cuenta admitidos en Cuentas solo en este directorio de la organización.
- En URI de redireccionamiento, cambie el desplegable a Aplicación de página única (SPA) y establezca el valor en
http://localhost:8080
.
- Establezca Nombre como
Seleccione Registrar. En la página Graph aplicación de prueba de función de Azure, copie los valores del identificador de aplicación (cliente) y del identificador de directorio (inquilino) y guárdelos, los necesitará en los pasos posteriores.
Registrar una aplicación para la función de Azure
Vuelva a Registros de aplicaciones y seleccione Nuevo registro. En la página Registrar una aplicación, establezca los valores siguientes.
- Establezca Nombre como
Graph Azure Function
. - Establezca los tipos de cuenta admitidos en Cuentas solo en este directorio de la organización.
- Deje uri de redireccionamiento en blanco.
- Establezca Nombre como
Seleccione Registrar. En la Graph función de Azure, copie el valor del identificador de aplicación (cliente) y guárdelo, lo necesitará en el paso siguiente.
Seleccione Certificados y secretos en Administrar. Seleccione el botón Nuevo secreto de cliente. Escriba un valor en Descripción, y seleccione una de las opciones para Expira, y después, Agregar.
Copie el valor del secreto del cliente antes de salir de esta página. Lo necesitará en el siguiente paso.
Importante
El secreto de cliente no se vuelve a mostrar, así que asegúrese de copiarlo en este momento.
Seleccione Permisos de API en Administrar. Elija Agregar un permiso.
Seleccione Microsoft Graph y, a continuación, Permisos delegados. Agregue Mail.Read y seleccione Agregar permisos.
Seleccione Exponer una API en Administrar y, a continuación, elija Agregar un ámbito.
Acepte el URI de id. de aplicación predeterminado y elija Guardar y continuar.
Rellene el formulario Agregar un ámbito de la siguiente manera:
- Nombre del ámbito: Mail.Read
- Quién¿puede dar su consentimiento?: Administradores y usuarios
- Nombre para mostrar del consentimiento de administrador: Leer las bandejas de entrada de todos los usuarios
- Descripción del consentimiento de administrador: Permite que la aplicación lea las bandejas de entrada de todos los usuarios
- Nombre para mostrar del consentimiento del usuario: Leer la bandeja de entrada
- Descripción del consentimiento del usuario: Permite que la aplicación lea la bandeja de entrada
- Estado: Habilitado
Seleccione Agregar ámbito.
Copie el nuevo ámbito, lo necesitará en pasos posteriores.
Seleccione Manifiesto en Administrar.
Busque
knownClientApplications
en el manifiesto y reemplace[]
su valor actual de por[TEST_APP_ID]
,TEST_APP_ID
donde se encuentra el identificador de aplicación del registro de la aplicación de prueba de función de Azure Graph de la aplicación. Haga clic en Guardar.
Nota
Agregar el identificador de aplicación de la knownClientApplications
aplicación de prueba a la propiedad en el manifiesto de la función de Azure permite que la aplicación de prueba desencadene un flujo de consentimiento combinado. Esto es necesario para que funcione el flujo en nombre del usuario.
Agregar ámbito de función de Azure para probar el registro de aplicaciones
Vuelva al registro Graph aplicación de prueba de función de Azure y seleccione Permisos de API en Administrar. Seleccione Agregar un permiso
Seleccione Mis API y, a continuación , seleccione Cargar más. Seleccione Graph función de Azure.
Seleccione el permiso Mail.Read y, a continuación, seleccione Agregar permisos.
En los permisos configurados, quite el permiso User.Read en Microsoft Graph seleccionando ... a la derecha del permiso y seleccionando Quitar permiso. Seleccione Sí, quite para confirmar.
Registrar una aplicación para el webhook de función de Azure
Vuelva a Registros de aplicaciones y seleccione Nuevo registro. En la página Registrar una aplicación, establezca los valores siguientes.
- Establezca Nombre como
Graph Azure Function Webhook
. - Establezca los tipos de cuenta admitidos en Cuentas solo en este directorio de la organización.
- Deje uri de redireccionamiento en blanco.
- Establezca Nombre como
Seleccione Registrar. En la Graph webhook de función de Azure, copie el valor del identificador de aplicación (cliente) y guárdelo, lo necesitará en el paso siguiente.
Seleccione Certificados y secretos en Administrar. Seleccione el botón Nuevo secreto de cliente. Escriba un valor en Descripción, y seleccione una de las opciones para Expira, y después, Agregar.
Copie el valor del secreto de cliente antes de salir de esta página. Lo necesitará en el siguiente paso.
Seleccione Permisos de API en Administrar. Elija Agregar un permiso.
Seleccione Microsoft Graph y, a continuación, Permisos de aplicación. Agregue User.Read.All y Mail.Read y, a continuación, seleccione Agregar permisos.
En los permisos configurados, quite el permiso User.Read delegado en Microsoft Graph seleccionando ... a la derecha del permiso y seleccionando Quitar permiso. Seleccione Sí, quite para confirmar.
Seleccione el botón Conceder consentimiento de administrador para... y, a continuación, seleccione Sí para conceder el consentimiento de administrador para los permisos de aplicación configurados. La columna Estado de la tabla Permisos configurados cambia a Concedido para ....
Implementar la API con autenticación en nombre de
En este ejercicio, finalizará la implementación de la función de Azure GetMyNewestMessage
y actualizará el cliente de prueba para llamar a la función.
La función azure usa el flujo en nombre del usuario. El orden básico de los eventos en este flujo son:
- La aplicación de prueba usa un flujo de autenticación interactivo para permitir al usuario iniciar sesión y conceder el consentimiento. Obtiene un token que está en el ámbito de la función de Azure. El token NO contiene ningún ámbito Graph Microsoft.
- La aplicación de prueba invoca la función de Azure y envía su token de acceso en el
Authorization
encabezado. - La función azure valida el token y, a continuación, intercambia ese token por un segundo token de acceso que contiene los ámbitos Graph Microsoft.
- La función Azure llama a Microsoft Graph en nombre del usuario mediante el segundo token de acceso.
Importante
Para evitar almacenar el identificador de aplicación y el secreto en el origen, usará el Administrador de secretos de .NET para almacenar estos valores. El Administrador de secretos solo tiene fines de desarrollo, las aplicaciones de producción deben usar un administrador de secretos de confianza para almacenar secretos.
Agregar autenticación a la aplicación de página única
Comience agregando autenticación al SPA. Esto permitirá a la aplicación obtener un token de acceso que conceda acceso para llamar a la función de Azure. Dado que se trata de un SPA, usará el flujo de código de autorización con PKCE.
Cree un nuevo archivo en el directorio TestClient denominado config.js y agregue el siguiente código.
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' ] }
Reemplace
YOUR_TEST_APP_APP_ID_HERE
por el identificador de aplicación que creó en Azure Portal para la Graph prueba de función de Azure. ReemplaceYOUR_TENANT_ID_HERE
por el valor de id. de directorio (espacio empresarial) que copió desde Azure Portal. ReemplaceYOUR_AZURE_FUNCTION_APP_ID_HERE
por el identificador de aplicación de la Graph función de Azure.Importante
Si usas el control de código fuente como git, ahora sería un buen momento para excluir el archivo config.js del control de código fuente para evitar la pérdida involuntaria de los identificadores de la aplicación y el identificador de inquilino.
Cree un nuevo archivo en el directorio TestClient denominado auth.js y agregue el siguiente código.
// 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(); }
Tenga en cuenta lo que hace este código.
- Inicializa un uso de
PublicClientApplication
los valores almacenados en config.js. - Se usa
loginPopup
para iniciar sesión con el ámbito de permisos de la función de Azure. - Almacena el nombre de usuario del usuario en la sesión.
Importante
Dado que la aplicación usa
loginPopup
, es posible que deba cambiar el bloqueador de elementos emergentes del explorador para permitir las ventanas emergentes dehttp://localhost:8080
.- Inicializa un uso de
Actualice la página e inicie sesión. La página debe actualizarse con el nombre de usuario, lo que indica que el inicio de sesión se ha realizado correctamente.
Agregar autenticación a la función de Azure
En esta sección, implementará el flujo en nombre de la GetMyNewestMessage
función de Azure para obtener un token de acceso compatible con Microsoft Graph.
Inicialice el almacén secreto de desarrollo de .NET abriendo la CLI en el directorio que contiene GraphTutorial.csproj y ejecutando el siguiente comando.
dotnet user-secrets init
Agregue el identificador de aplicación, el secreto y el identificador de inquilino al almacén de secretos mediante los siguientes comandos. Reemplace
YOUR_API_FUNCTION_APP_ID_HERE
por el identificador de aplicación de la Graph función de Azure. ReemplaceYOUR_API_FUNCTION_APP_SECRET_HERE
por el secreto de aplicación que creó en Azure Portal para la Graph función de Azure. ReemplaceYOUR_TENANT_ID_HERE
por el valor de id. de directorio (espacio empresarial) que copió desde Azure Portal.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"
Procesar el token de portador entrante
En esta sección, implementará una clase para validar y procesar el token de portador enviado desde el SPA a la función de Azure.
Cree un nuevo directorio en el directorio GraphTutorial denominado Autenticación.
Cree un nuevo archivo denominado TokenValidationResult.cs en la carpeta ./GraphTutorial/Authentication y agregue el siguiente código.
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; } } }
Cree un nuevo archivo denominado TokenValidation.cs en la carpeta ./GraphTutorial/Authentication y agregue el siguiente código.
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; } } }
Tenga en cuenta lo que hace este código.
- Se asegura de que hay un token de portador en el
Authorization
encabezado. - Comprueba la firma y el emisor de la configuración de OpenID publicada de Azure.
- Comprueba que la audiencia (
aud
notificación) coincide con el identificador de aplicación de la función de Azure. - Analiza el token y genera un identificador de cuenta MSAL, que será necesario para aprovechar el almacenamiento en caché de tokens.
Crear un proveedor de autenticación en nombre de
Cree un nuevo archivo en el directorio de autenticación denominado OnBehalfOfAuthProvider.cs y agregue el siguiente código a ese archivo.
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); } } }
Tómese un momento para considerar lo que hace el código en OnBehalfOfAuthProvider.cs .
- En la
GetAccessToken
función, primero intenta obtener un token de usuario de la memoria caché de tokens medianteAcquireTokenSilent
. Si se produce un error, usa el token de portador enviado por la aplicación de prueba a la función de Azure para generar una aserción de usuario. A continuación, usa esa aserción de usuario para obtener un token compatible Graph medianteAcquireTokenOnBehalfOf
. - Implementa la interfaz
Microsoft.Graph.IAuthenticationProvider
, lo que permite que esta clase se pase en el constructor de laGraphServiceClient
para autenticar las solicitudes salientes.
Implementar un servicio Graph cliente
En esta sección, implementará un servicio que se puede registrar para la inserción de dependencias. El servicio se usará para obtener un cliente Graph autenticado.
Cree un nuevo directorio en el directorio GraphTutorial denominado Services.
Cree un nuevo archivo en el directorio servicios denominado IGraphClientService.cs y agregue el siguiente código a ese archivo.
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); } }
Cree un nuevo archivo en el directorio Servicios denominado GraphClientService.cs y agregue el siguiente código a ese archivo.
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 { } }
Agregue las siguientes propiedades a la
GraphClientService
clase.// 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;
Agregue las siguientes funciones a la
GraphClientService
clase.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); }
Agregue una implementación de marcador de posición para la
GetAppGraphClient
función. Lo implementará en secciones posteriores.public GraphServiceClient GetAppGraphClient() { throw new System.NotImplementedException(); }
La
GetUserGraphClient
función toma los resultados de la validación de tokens y crea una autenticaciónGraphServiceClient
para el usuario.Abra ./GraphTutorial/Program.cs y reemplace su contenido por lo siguiente.
Este código agregará secretos de usuario a la configuración y habilitará la inserción de dependencias en las funciones de Azure, exponiendo el
GraphClientService
servicio.
Implementar la función GetMyNewestMessage
Abra ./GraphTutorial/GetMyNewestMessage.cs y reemplace todo su contenido por lo siguiente.
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]); } } }
Revisar el código en GetMyNewestMessage.cs
Tómese un momento para considerar lo que hace el código en GetMyNewestMessage.cs .
- En el constructor, guarda los objetos
IConfiguration
yIGraphClientService
pasados a través de la inserción de dependencias. - En la
Run
función, hace lo siguiente:- Valida que los valores de configuración necesarios estén presentes en el
IConfiguration
objeto. - Valida el token portador y devuelve un código
401
de estado si el token no es válido. - Obtiene un Graph del usuario
GraphClientService
que realizó esta solicitud. - Usa microsoft Graph SDK para obtener el mensaje más reciente de la bandeja de entrada del usuario y lo devuelve como un cuerpo JSON en la respuesta.
- Valida que los valores de configuración necesarios estén presentes en el
Llamar a la función de Azure desde la aplicación de prueba
Abra auth.js y agregue la siguiente función para obtener un token de acceso.
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; } } }
Tenga en cuenta lo que hace este código.
- Primero intenta obtener un token de acceso de forma silenciosa, sin la interacción del usuario. Dado que el usuario ya debe haber iniciado sesión, MSAL debe tener tokens para el usuario en su caché.
- Si se produce un error que indica que el usuario necesita interactuar, intenta obtener un token de forma interactiva.
Sugerencia
Puede analizar el token https://jwt.ms
aud
de acceso en y confirmar que la notificación es el identificador de la aplicación para la función de Azure yscp
que la notificación contiene el ámbito de permisos de la función azure, no Microsoft Graph.Cree un nuevo archivo en el directorio TestClient denominado azurefunctions.js y agregue el siguiente código.
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 }); } }
Cambie el directorio actual de la CLI al directorio ./GraphTutorial y ejecute el siguiente comando para iniciar la función de Azure localmente.
func start
Si aún no sirve el SPA, abra una segunda ventana de la CLI y cambie el directorio actual al directorio ./TestClient . Ejecute el siguiente comando para ejecutar la aplicación de prueba.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate"
Abra el explorador y vaya a
http://localhost:8080
. Inicie sesión y seleccione el elemento de navegación Mensaje más reciente. La aplicación muestra información sobre el mensaje más reciente en la bandeja de entrada del usuario.
Implementar el webhook con autenticación de credenciales de cliente
En este ejercicio, finalizará la implementación de Azure Functions SetSubscription
Notify
y , y actualizará la aplicación de prueba para suscribirse y cancelar la suscripción a los cambios en la bandeja de entrada de un usuario.
- La
SetSubscription
función actuará como una API, lo que permite a la aplicación de prueba crear o eliminar una suscripción a los cambios en la bandeja de entrada de un usuario. - La
Notify
función actuará como el webhook que recibe notificaciones de cambios generadas por la suscripción.
Ambas funciones usarán el flujo de concesión de credenciales de cliente para obtener un token de solo aplicación para llamar a Microsoft Graph. Dado que un administrador concedió el consentimiento de administrador a los ámbitos de permisos necesarios, no será necesaria ninguna interacción del usuario para obtener el token.
Agregar autenticación de credenciales de cliente al proyecto de Azure Functions
En esta sección, implementará el flujo de credenciales de cliente en el proyecto de Azure Functions para obtener un token de acceso compatible con Microsoft Graph.
Abra la CLI en el directorio que contiene GraphTutorial.csproj.
Agregue el identificador y el secreto de la aplicación de webhook al almacén secreto mediante los siguientes comandos. Reemplace
YOUR_WEBHOOK_APP_ID_HERE
por el identificador de aplicación del Graph Webhook de función de Azure. ReemplaceYOUR_WEBHOOK_APP_SECRET_HERE
por el secreto de aplicación que creó en Azure Portal para Graph Webhook de función de Azure.dotnet user-secrets set webHookId "YOUR_WEBHOOK_APP_ID_HERE" dotnet user-secrets set webHookSecret "YOUR_WEBHOOK_APP_SECRET_HERE"
Crear un proveedor de autenticación de credenciales de cliente
Cree un nuevo archivo en el directorio ./GraphTutorial/Authentication denominado ClientCredentialsAuthProvider.cs y agregue el siguiente código.
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); } } }
Tómese un momento para considerar lo que hace el código en ClientCredentialsAuthProvider.cs .
- En el constructor, inicializa un ConfidentialClientApplication desde el
Microsoft.Identity.Client
paquete. Usa las funcionesWithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
y.WithTenantId(tenantId)
para restringir la audiencia de inicio de sesión solo a la organización Microsoft 365 especificada. - En la
GetAccessToken
función, llama paraAcquireTokenForClient
obtener un token para la aplicación. El flujo de tokens de credenciales de cliente siempre no es interactivo. - Implementa la interfaz
Microsoft.Graph.IAuthenticationProvider
, lo que permite que esta clase se pase en el constructor de laGraphServiceClient
para autenticar las solicitudes salientes.
Actualizar GraphClientService
Abra GraphClientService.cs y agregue la siguiente propiedad a la clase.
private GraphServiceClient _appGraphClient;
Reemplace la función
GetAppGraphClient
existente por lo siguiente.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; }
Implementar función Notify
En esta sección, implementará la función Notify
, que se usará como la dirección URL de notificación para las notificaciones de cambios.
Cree un nuevo directorio en el directorio GraphTutorials denominado Models.
Cree un nuevo archivo en el directorio Models denominado ResourceData.cs y agregue el siguiente código.
namespace GraphTutorial.Models { // Class to represent the resourceData object // inside a change notification public class ResourceData { public string Id { get;set; } } }
Cree un nuevo archivo en el directorio Models denominado ChangeNotificationPayload.cs y agregue el siguiente código.
Cree un nuevo archivo en el directorio Models denominado NotificationList.cs y agregue el siguiente código.
namespace GraphTutorial.Models { // Class representing an array of notifications // in a notification payload public class NotificationList { public ChangeNotification[] Value { get;set; } } }
Abra ./GraphTutorial/Notify.cs y reemplace todo su contenido por lo siguiente.
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}"); } } }
Tómese un momento para considerar lo que hace el código en Notify.cs .
- La
Run
función comprueba la presencia de un parámetrovalidationToken
de consulta. Si ese parámetro está presente, procesa la solicitud como una solicitud de validación y responde en consecuencia. - Si la solicitud no es una solicitud de validación, la carga JSON se deserializa en un
ChangeNotificationCollection
. - Cada notificación de la lista se comprueba para el valor de estado de cliente esperado y se procesa.
- El mensaje que desencadenó la notificación se recupera con Microsoft Graph.
Implementar la función SetSubscription
En esta sección, implementará la función SetSubscription. Esta función actuará como una API a la que la aplicación de prueba llama para crear o eliminar una suscripción en la bandeja de entrada de un usuario.
Cree un nuevo archivo en el directorio Models denominado SetSubscriptionPayload.cs y agregue el siguiente código.
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; } } }
Abra ./GraphTutorial/SetSubscription.cs y reemplace todo su contenido por lo siguiente.
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(); } } } }
Tómese un momento para considerar lo que hace el código de SetSubscription.cs .
- La
Run
función lee la carga JSON enviada en la solicitud POST para determinar el tipo de solicitud (suscribirse o cancelar suscripción), el identificador de usuario al que suscribirse y el identificador de suscripción para cancelar la suscripción. - Si la solicitud es una solicitud de suscripción, usa el SDK de Microsoft Graph para crear una nueva suscripción en la bandeja de entrada del usuario especificado. La suscripción notificará cuándo se crean o actualizan los mensajes. La nueva suscripción se devuelve en la carga JSON de la respuesta.
- Si la solicitud es una solicitud de cancelación de suscripción, usa el SDK de Microsoft Graph para eliminar la suscripción especificada.
Llamar a SetSubscription desde la aplicación de prueba
En esta sección, implementarás funciones para crear y eliminar suscripciones en la aplicación de prueba.
Abra ./TestClient/azurefunctions.js y agregue la siguiente función.
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 }); }
Este código llama a la
SetSubscription
función de Azure para suscribirse y agrega la nueva suscripción a la matriz de suscripciones de la sesión.Agregue la siguiente función a 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 }); }
Este código llama a la
SetSubscription
función de Azure para cancelar la suscripción y quita la suscripción de la matriz de suscripciones de la sesión.Si no tiene ngrok en ejecución, ejecute ngrok (
ngrok http 7071
) y copie la dirección URL de reenvío HTTPS.Agregue la dirección URL de ngrok al almacén de secretos de usuario ejecutando el siguiente comando.
dotnet user-secrets set ngrokUrl "YOUR_NGROK_URL_HERE"
Importante
Si reinicia ngrok, tendrá que repetir este comando para actualizar la dirección URL de ngrok.
Cambie el directorio actual de la CLI al directorio ./GraphTutorial y ejecute el siguiente comando para iniciar la función de Azure localmente.
func start
Actualice el SPA y seleccione el elemento de navegación Suscripciones . Escriba un identificador de usuario para un usuario de la Microsoft 365 que tenga un buzón Exchange Online usuario. Puede ser del usuario
id
(de Microsoft Graph) o del usuariouserPrincipalName
. Haga clic en Suscribirse.La página se actualiza mostrando la nueva suscripción en la tabla.
Enviar un correo electrónico al usuario. Después de un breve período de tiempo, se
Notify
debe llamar a la función. Puede comprobarlo en la interfaz web de ngrok (http://localhost:4040
) o en el resultado de depuración del proyecto de función de 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) ...
En la aplicación de prueba, haz clic en Eliminar en la fila de la tabla de la suscripción. La página se actualiza y la suscripción ya no está en la tabla.
Prepararse para publicar en Azure
En este ejercicio, aprenderás sobre los cambios que se necesitan en la función de Azure de ejemplo para preparar la publicación en una aplicación de Azure Functions.
Actualización del código
La configuración se lee desde el almacén secreto de usuario, que solo se aplica a la máquina de desarrollo. Antes de publicar en Azure, deberá cambiar el lugar donde almacena la configuración y actualizar el código en Program.cs en consecuencia.
Los secretos de aplicación deben almacenarse en almacenamiento seguro, como Azure Key Vault.
Actualizar la configuración de CORS para la función de Azure
En este ejemplo, configuramos CORS en local.settings.json para permitir que la aplicación de prueba llame a la función. Tendrás que configurar la función publicada para permitir cualquier aplicación SPA que la llame.
Actualizar registros de aplicaciones
La knownClientApplications
propiedad del manifiesto del registro de la aplicación Graph Azure Function tendrá que actualizarse con los IDs de la aplicación de cualquier aplicación que llame a la función de Azure.
Volver a crear suscripciones existentes
Las suscripciones creadas con la dirección URL de webhook en el equipo local o ngrok deben volver a crearse con la dirección URL de producción de la Notify
función de Azure.
¡Enhorabuena!
Ha completado el tutorial de Microsoft Graph Funciones de Azure. Ahora que tienes una aplicación de trabajo que llama a Microsoft Graph, puedes experimentar y agregar nuevas características. Visite la información general de Microsoft Graph para ver todos los datos a los que puede acceder con Microsoft Graph.
Comentarios
Proporcione cualquier comentario sobre este tutorial en el repositorio GitHub usuario.
¿Tiene algún problema con esta sección? Si es así, envíenos sus comentarios para que podamos mejorarla.