Criar funções do Azure com o Microsoft Graph
Este tutorial ensina como criar uma Função do Azure que usa a API do Microsoft Graph para recuperar informações de calendário para um usuário.
Dica
Se você preferir apenas baixar o tutorial concluído, poderá baixar ou clonar o GitHub repositório. Consulte o arquivo README na pasta de demonstração para obter instruções sobre como configurar o aplicativo com uma ID do aplicativo e segredo.
Pré-requisitos
Antes de iniciar este tutorial, você deve ter as seguintes ferramentas instaladas em sua máquina de desenvolvimento.
Você também deve ter uma conta de estudante ou de trabalho da Microsoft, com acesso a uma conta de administrador global na mesma organização. Se você não tiver uma conta da Microsoft, poderá inscrever-se no programa de desenvolvedor Microsoft 365 para obter uma assinatura Office 365 gratuita.
Observação
Este tutorial foi escrito com as seguintes versões das ferramentas acima. As etapas neste guia podem funcionar com outras versões, mas que não foram testadas.
- .NET Core SDK 5.0.203
- Ferramentas principais do Azure Functions 3.0.3442
- Azure CLI 2.23.0
- ngrok 2.3.40
Comentários
Forneça qualquer comentário sobre este tutorial no repositório GitHub.
Criar um projeto de funções do Azure
Neste tutorial, você criará uma função simples do Azure que implementa funções de gatilho HTTP que chamam a Microsoft Graph. Essas funções abrangem os seguintes cenários:
- Implementa uma API para acessar a caixa de entrada de um usuário usando a autenticação em nome do fluxo .
- Implementa uma API para assinar e cancelar a assinatura para notificações na caixa de entrada de um usuário, usando credenciais de cliente concedem autenticação de fluxo.
- Implementa um webhook para receber notificações de alteração do Microsoft Graph e acessar dados usando credenciais do cliente concedem fluxo.
Você também criará um aplicativo javaScript simples de página única (SPA) para chamar as APIs implementadas na Função Azure.
Criar projeto funções do Azure
Abra sua interface de linha de comando (CLI) em um diretório onde você deseja criar o projeto. Execute o seguinte comando:
func init GraphTutorial --worker-runtime dotnetisolated
Altere o diretório atual em sua CLI para o diretório GraphTutorial e execute os seguintes comandos para criar três funções no projeto.
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 e adicione o seguinte ao arquivo para permitir o CORS de
http://localhost:8080
, a URL do aplicativo de teste."Host": { "CORS": "http://localhost:8080" }
Execute o seguinte comando para executar o projeto localmente.
func start
Se tudo estiver funcionando, você verá a seguinte saída:
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
Verifique se as funções estão funcionando corretamente abrindo seu navegador e navegando para as URLs de função mostradas na saída. Você deve ver a seguinte mensagem no navegador:
Welcome to Azure Functions!
.
Criar aplicativo de página única
Abra sua CLI em um diretório onde você deseja criar o projeto. Crie um diretório chamado TestClient para manter seus arquivos HTML e JavaScript.
Crie um novo arquivo chamado index.html no diretório TestClient e adicione o código a seguir.
<!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>
Isso define o layout básico do aplicativo, incluindo uma barra de navegação. Ele também adiciona o seguinte:
- Bootstrap e seu JavaScript de suporte
- FontAwesome
- Biblioteca de Autenticação da Microsoft para JavaScript (MSAL.js) 2.0
Dica
A página inclui um favicon, (
<link rel="shortcut icon" href="g-raph.png">
). Você pode remover essa linha ou baixar o arquivo g-raph.png de GitHub.Crie um novo arquivo chamado style.css no diretório TestClient e adicione o código a seguir.
body { padding-top: 70px; }
Crie um novo arquivo chamado ui.js no diretório TestClient e adicione o código a seguir.
// 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 renderizar a página atual com base no exibição selecionado.
Testar o aplicativo de página única
Observação
Esta seção inclui instruções para usar o dotnet-serve para executar um servidor HTTP de teste simples em sua máquina de desenvolvimento. Não é necessário usar essa ferramenta específica. Você pode usar qualquer servidor de teste que preferir para atender ao diretório TestClient .
Execute o seguinte comando em sua CLI para instalar dotnet-serve.
dotnet tool install --global dotnet-serve
Altere o diretório atual em sua CLI para o diretório TestClient e execute o seguinte comando para iniciar um servidor HTTP.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate" -p 8080
Abra o navegador e vá até
http://localhost:8080
. A página deve renderizar, mas nenhum dos botões funciona no momento.
Adicionar pacotes NuGet
Antes de continuar, instale alguns pacotes NuGet que você usará posteriormente.
- Microsoft.Azure.Functions.Extensions para habilitar a injeção de dependência no projeto funções do Azure.
- Microsoft.Extensions.Configuration.UserSecrets para ler a configuração do aplicativo no armazenamento secreto de desenvolvimento do .NET.
- Microsoft.Graph para fazer chamadas para o Microsoft Graph.
- Microsoft.Identity.Client para autenticar e gerenciar tokens.
- Microsoft.IdentityModel.Protocols.OpenIdConnect para recuperar a configuração do OpenID para validação de token.
- System.IdentityModel.Tokens.Jwt para validar tokens enviados para a API da Web.
Altere o diretório atual em sua CLI para o diretório GraphTutorial e execute os seguintes 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 os aplicativos no portal
Neste exercício, você criará três novos aplicativos do Azure AD usando o Azure Active Directory de administração:
- Um registro de aplicativo para o aplicativo de página única para que ele possa entrar nos usuários e obter tokens permitindo que o aplicativo chame a Função Azure.
- Um registro de aplicativo para a Função do Azure que permite que ele use o fluxo em nome do fluxo para trocar o token enviado pelo SPA para um token que permitirá que ele chame o Microsoft Graph.
- Um registro de aplicativo para o webhook da Função do Azure que permite que ele use o fluxo de credenciais do cliente para chamar o Microsoft Graph sem um usuário.
Observação
Este exemplo requer três registros de aplicativo porque está implementando o fluxo em nome do fluxo e o fluxo de credenciais do cliente. Se a função do Azure usar apenas um desses fluxos, você só precisará criar os registros de aplicativos que correspondam a esse fluxo.
Abra um navegador e navegue até o centro de administração Azure Active Directory e faça logon usando um administrador Microsoft 365 da organização de locatários.
Selecione Azure Active Directory na navegação esquerda e selecione Registros de aplicativos em Gerenciar.
Registrar um aplicativo para o aplicativo de página única
Selecione Novo registro. Na página Registrar um aplicativo, defina os valores da seguinte forma.
- Defina Nome para
Graph Azure Function Test App
. - De definir tipos de conta com suporte apenas para Contas neste diretório organizacional.
- Em URI de redirecionamento, altere o menu suspenso para SPA (aplicativo de página única) e despe o valor como
http://localhost:8080
.
- Defina Nome para
Selecione Registrar. Na página Graph Aplicativo de Teste de Função do Azure, copie os valores da ID de Aplicativo (cliente) e ID de Diretório (locatário) e salve-os, você precisará deles nas etapas posteriores.
Registrar um aplicativo para a função Azure
Retorne aos Registros de Aplicativos e selecione Novo registro. Na página Registrar um aplicativo, defina os valores da seguinte forma.
- Defina Nome para
Graph Azure Function
. - De definir tipos de conta com suporte apenas para Contas neste diretório organizacional.
- Deixe o URI de redirecionamento em branco.
- Defina Nome para
Selecione Registrar. Na página Graph função do Azure, copie o valor da ID do Aplicativo (cliente) e salve-a, você precisará dela na próxima etapa.
Selecione Certificados e segredos sob Gerenciar. Selecione o botão Novo segredo do cliente. Insira um valor em Descrição e selecione uma das opções para Expira em e selecione Adicionar.
Copie o valor de segredo do cliente antes de sair desta página. Você precisará dele na próxima etapa.
Importante
Este segredo do cliente nunca é mostrado novamente, portanto, certifique-se de copiá-lo agora.
Selecione Permissões de API em Gerenciar. Escolha Adicionar uma permissão.
Selecione Microsoft Graph e Permissões Delegadas. Adicione Mail.Read e selecione Adicionar permissões.
Selecione Expor uma API em Gerenciar e escolha Adicionar um escopo.
Aceite o URI de ID de aplicativo padrão e escolha Salvar e continuar.
Preencha o formulário Adicionar um escopo da seguinte maneira:
- Nome do escopo: Mail.Read
- Who podem consentir?: Administradores e usuários
- Nome de exibição de consentimento do administrador: Ler caixas de entrada de todos os usuários
- Descrição do consentimento do administrador: Permite que o aplicativo leia as caixas de entrada de todos os usuários
- Nome de exibição de consentimento do usuário: Ler sua caixa de entrada
- Descrição do consentimento do usuário: Permite que o aplicativo leia sua caixa de entrada
- Estado: Habilitado
Selecione Adicionar escopo.
Copie o novo escopo, você precisará dele em etapas posteriores.
Selecione Manifesto em Gerenciar.
Localize
knownClientApplications
no manifesto e[TEST_APP_ID]``[]
substitua o valor atual de ,TEST_APP_ID
onde está a ID do aplicativo do registro do aplicativo de teste de função Graph Azure. Selecione Salvar.
Observação
Adicionar a ID knownClientApplications
do aplicativo de teste à propriedade no manifesto do Azure Function permite que o aplicativo de teste acione um fluxo de consentimento combinado. Isso é necessário para que o fluxo em nome do trabalho funcione.
Adicionar escopo de função do Azure para testar o registro do aplicativo
Retorne ao aplicativo de teste Graph função do Azure e selecione Permissões de API em Gerenciar. Selecione Adicionar uma permissão.
Selecione Minhas APIs e, em seguida, selecione Carregar mais. Selecione Graph função do Azure.
Selecione a permissão Mail.Read e selecione Adicionar permissões.
Nas permissões Configuradas, remova a permissão User.Read em Microsoft Graph selecionando o ... à direita da permissão e selecionando Remover permissão. Selecione Sim, remova para confirmar.
Registrar um aplicativo para o webhook da Função do Azure
Retorne aos Registros de Aplicativos e selecione Novo registro. Na página Registrar um aplicativo, defina os valores da seguinte forma.
- Defina Nome para
Graph Azure Function Webhook
. - De definir tipos de conta com suporte apenas para Contas neste diretório organizacional.
- Deixe o URI de redirecionamento em branco.
- Defina Nome para
Selecione Registrar. Na página Graph webhook da Função do Azure, copie o valor da ID do Aplicativo (cliente) e salve-a, você precisará dela na próxima etapa.
Selecione Certificados e segredos sob Gerenciar. Selecione o botão Novo segredo do cliente. Insira um valor em Descrição e selecione uma das opções para Expira em e selecione Adicionar.
Copie o valor secreto do cliente antes de sair desta página. Você precisará dele na próxima etapa.
Selecione Permissões de API em Gerenciar. Escolha Adicionar uma permissão.
Selecione Microsoft Graph e Permissões de Aplicativo. Adicione User.Read.All e Mail.Read e selecione Adicionar permissões.
Nas permissões Configuradas, remova a permissão User.Read delegada em Microsoft Graph selecionando o ... à direita da permissão e selecionando Remover permissão. Selecione Sim, remova para confirmar.
Selecione o botão Conceder consentimento do administrador para... e selecione Sim para conceder o consentimento do administrador para as permissões de aplicativo configuradas. A coluna Status na tabela Permissões Configuradas muda para Concedido para ....
Implementar a API com autenticação em nome da
Neste exercício, você terminará de implementar a Função do Azure GetMyNewestMessage
e atualizará o cliente de teste para chamar a função.
A Função do Azure usa o fluxo em nome do fluxo. A ordem básica de eventos nesse fluxo são:
- O aplicativo de teste usa um fluxo de auth interativo para permitir que o usuário entre e conceda consentimento. Ele obtém de volta um token que é escopo para a função do Azure. O token NÃO contém escopos Graph Microsoft.
- O aplicativo de teste invoca a Função Azure, enviando seu token de acesso no
Authorization
header. - A Função do Azure valida o token e, em seguida, troca esse token por um segundo token de acesso que contém escopos Graph Microsoft.
- A Função do Azure chama a Microsoft Graph em nome do usuário usando o segundo token de acesso.
Importante
Para evitar armazenar a ID do aplicativo e o segredo na origem, você usará o .NET Secret Manager para armazenar esses valores. O Gerenciador Secreto é apenas para fins de desenvolvimento, os aplicativos de produção devem usar um gerente secreto confiável para armazenar segredos.
Adicionar autenticação ao aplicativo de página única
Comece adicionando autenticação ao SPA. Isso permitirá que o aplicativo receba um token de acesso concedendo acesso para chamar a Função Azure. Como se trata de um SPA, ele usará o fluxo de código de autorização com PKCE.
Crie um novo arquivo no diretório TestClient chamado config.js adicionar o código a seguir.
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' ] }
Substitua
YOUR_TEST_APP_APP_ID_HERE
pela ID do aplicativo que você criou no portal do Azure para o aplicativo de teste de função Graph Azure. SubstituaYOUR_TENANT_ID_HERE
pelo valor de ID de Diretório (locatário) copiado do portal do Azure. SubstituaYOUR_AZURE_FUNCTION_APP_ID_HERE
pela ID do aplicativo para a função Graph Azure.Importante
Se você estiver usando o controle de origem, como git, agora seria um bom momento para excluir o arquivo config.js do controle de origem para evitar o vazamento inadvertida das IDs do aplicativo e da ID do locatário.
Crie um novo arquivo no diretório TestClient chamado auth.js adicionar o código a seguir.
// 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(); }
Considere o que esse código faz.
- Ele inicializa um usando os
PublicClientApplication
valores armazenados em config.js. - Ele usa
loginPopup
para entrar no usuário, usando o escopo de permissão para a Função Azure. - Ele armazena o nome de usuário do usuário na sessão.
Importante
Como o aplicativo usa
loginPopup
, talvez seja necessário alterar o bloqueador pop-up do navegador para permitir pop-ups dehttp://localhost:8080
.- Ele inicializa um usando os
Atualize a página e entre. A página deve ser atualizada com o nome de usuário, indicando que a assinatura foi bem-sucedida.
Adicionar autenticação à função do Azure
Nesta seção, você implementará o fluxo em nome do fluxo GetMyNewestMessage
na Função Azure para obter um token de acesso compatível com o Microsoft Graph.
Inicialize o armazenamento secreto de desenvolvimento do .NET abrindo sua CLI no diretório que contém GraphTutorial.csproj e executando o seguinte comando.
dotnet user-secrets init
Adicione a ID do aplicativo, o segredo e a ID do locatário ao armazenamento secreto usando os comandos a seguir. Substitua
YOUR_API_FUNCTION_APP_ID_HERE
pela ID do aplicativo para a função Graph Azure. SubstituaYOUR_API_FUNCTION_APP_SECRET_HERE
pelo segredo do aplicativo criado no portal do Azure para a função Graph Azure. SubstituaYOUR_TENANT_ID_HERE
pelo valor de ID de Diretório (locatário) copiado do portal do 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"
Processar o token de portador de entrada
Nesta seção, você implementará uma classe para validar e processar o token de portador enviado do SPA para a Função do Azure.
Crie um novo diretório no diretório GraphTutorial chamado Authentication.
Crie um novo arquivo chamado TokenValidationResult.cs na pasta ./GraphTutorial/Authentication e adicione o código a seguir.
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; } } }
Crie um novo arquivo chamado TokenValidation.cs na pasta ./GraphTutorial/Authentication e adicione o código a seguir.
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; } } }
Considere o que esse código faz.
- Ele garante que haja um token de portador no
Authorization
header. - Ele verifica a assinatura e o emissor da configuração openID publicada do Azure.
- Verifica se a audiência (
aud
declaração) corresponde à ID do aplicativo do Azure Function. - Ele analisará o token e gerará uma ID de conta MSAL, que será necessária para tirar proveito do cache de token.
Criar um provedor de autenticação em nome do
Crie um novo arquivo no diretório autenticação chamado OnBehalfOfAuthProvider.cs e adicione o código a seguir a esse arquivo.
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); } } }
Considere o que o código em OnBehalfOfAuthProvider.cs faz.
- Na função
GetAccessToken
, ele tenta primeiro obter um token de usuário do cache de token usandoAcquireTokenSilent
. Se isso falhar, ele usará o token de portador enviado pelo aplicativo de teste para a Função do Azure para gerar uma afirmação do usuário. Em seguida, ele usa essa afirmação do usuário para obter um token Graph compatível com a Graph usandoAcquireTokenOnBehalfOf
. - Ele implementa a
Microsoft.Graph.IAuthenticationProvider
interface, permitindo que essa classe seja passada no construtor dasGraphServiceClient
solicitações de saída autenticadas.
Implementar um serviço Graph cliente
Nesta seção, você implementará um serviço que pode ser registrado para injeção de dependência. O serviço será usado para obter um cliente Graph autenticado.
Crie um novo diretório no diretório GraphTutorial chamado Services.
Crie um novo arquivo no diretório Serviços chamado IGraphClientService.cs e adicione o código a seguir a esse arquivo.
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); } }
Crie um novo arquivo no diretório Serviços chamado GraphClientService.cs e adicione o código a seguir a esse arquivo.
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 { } }
Adicione as seguintes propriedades à
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;
Adicione as seguintes funções à
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); }
Adicione uma implementação de espaço reservado para a
GetAppGraphClient
função. Você implementará isso em seções posteriores.public GraphServiceClient GetAppGraphClient() { throw new System.NotImplementedException(); }
A
GetUserGraphClient
função pega os resultados da validação de token e cria um autenticadoGraphServiceClient
para o usuário.Abra ./GraphTutorial/Program.cs e substitua seu conteúdo pelo seguinte.
Este código adicionará segredos de usuário à configuração e habilita a injeção de dependência em suas Funções do Azure, expondo o
GraphClientService
serviço.
Implementar a função GetMyNewestMessage
Abra ./GraphTutorial/GetMyNewestMessage.cs e substitua todo o conteúdo pelo seguinte.
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]); } } }
Revise o código em GetMyNewestMessage.cs
Considere o que o código em GetMyNewestMessage.cs faz.
- No construtor, ele salva os objetos e passados
IConfiguration
IGraphClientService
por injeção de dependência. - Na função
Run
, ele faz o seguinte:- Valida se os valores de configuração necessários estão presentes no
IConfiguration
objeto. - Valida o token do portador e retorna um
401
código de status se o token for inválido. - Obtém Graph cliente do
GraphClientService
usuário que fez essa solicitação. - Usa o Microsoft Graph SDK para obter a mensagem mais recente da caixa de entrada do usuário e retorna-a como um corpo JSON na resposta.
- Valida se os valores de configuração necessários estão presentes no
Chamar a função do Azure do aplicativo de teste
Abra auth.js e adicione a função a seguir para obter um token de acesso.
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; } } }
Considere o que esse código faz.
- Ele tenta primeiro obter um token de acesso silenciosamente, sem interação do usuário. Como o usuário já deve estar assinado, o MSAL deve ter tokens para o usuário em seu cache.
- Se isso falhar com um erro que indique que o usuário precisa interagir, ele tentará obter um token interativamente.
Dica
Você pode analisar o token de acesso https://jwt.ms
aud
em e confirmar se a declaração é a ID do aplicativo para a Função Azure escp
que a declaração contém o escopo de permissão da Função do Azure, não o Microsoft Graph.Crie um novo arquivo no diretório TestClient chamado azurefunctions.js adicionar o código a seguir.
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 }); } }
Altere o diretório atual em sua CLI para o diretório ./GraphTutorial e execute o seguinte comando para iniciar a Função do Azure localmente.
func start
Se ainda não estiver servindo o SPA, abra uma segunda janela CLI e altere o diretório atual para o diretório ./TestClient . Execute o seguinte comando para executar o aplicativo de teste.
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate"
Abra o navegador e vá até
http://localhost:8080
. Entre e selecione o item de navegação Mensagem Mais Recente. O aplicativo exibe informações sobre a mensagem mais recente na caixa de entrada do usuário.
Implementar o webhook com autenticação de credenciais de cliente
Neste exercício, você terminará de implementar as Funções do Azure SetSubscription
Notify
e , e atualizará o aplicativo de teste para assinar e cancelar a assinatura de alterações na caixa de entrada de um usuário.
- A
SetSubscription
função atuará como uma API, permitindo que o aplicativo de teste crie ou exclua uma assinatura para alterações na caixa de entrada de um usuário. - A
Notify
função atuará como o webhook que recebe notificações de alteração geradas pela assinatura.
Ambas as funções usarão o fluxo de concessão de credenciais do cliente para obter um token somente de aplicativo para chamar a Microsoft Graph. Como um administrador concedeu consentimento ao administrador para os escopos de permissão necessários, nenhuma interação do usuário será necessária para obter o token.
Adicionar autenticação de credenciais de cliente ao projeto funções do Azure
Nesta seção, você implementará o fluxo de credenciais do cliente no projeto Funções do Azure para obter um token de acesso compatível com o Microsoft Graph.
Abra sua CLI no diretório que contém GraphTutorial.csproj.
Adicione a ID do aplicativo webhook e o segredo ao armazenamento secreto usando os comandos a seguir. Substitua
YOUR_WEBHOOK_APP_ID_HERE
pela ID do aplicativo para o webhook de função Graph Azure. SubstituaYOUR_WEBHOOK_APP_SECRET_HERE
pelo segredo do aplicativo criado no portal do Azure para o webhook de função Graph Azure.dotnet user-secrets set webHookId "YOUR_WEBHOOK_APP_ID_HERE" dotnet user-secrets set webHookSecret "YOUR_WEBHOOK_APP_SECRET_HERE"
Criar um provedor de autenticação de credenciais de cliente
Crie um novo arquivo no diretório ./GraphTutorial/Authentication chamado ClientCredentialsAuthProvider.cs e adicione o código a seguir.
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); } } }
Considere o que o código em ClientCredentialsAuthProvider.cs faz.
- No construtor, ele inicializa um ConfidentialClientApplication do
Microsoft.Identity.Client
pacote. Ele usa as funçõesWithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
e.WithTenantId(tenantId)
para restringir o público de logon apenas para a organização Microsoft 365 especificada. - Na função
GetAccessToken
, ele chama paraAcquireTokenForClient
obter um token para o aplicativo. O fluxo de tokens de credenciais do cliente é sempre não interativo. - Ele implementa a
Microsoft.Graph.IAuthenticationProvider
interface, permitindo que essa classe seja passada no construtor dasGraphServiceClient
solicitações de saída autenticadas.
Atualizar GraphClientService
Abra GraphClientService.cs e adicione a seguinte propriedade à classe.
private GraphServiceClient _appGraphClient;
Substitua a função
GetAppGraphClient
existente pela seguinte.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 função Notify
Nesta seção, você implementará a função Notify
, que será usada como a URL de notificação para notificações de alteração.
Crie um novo diretório no diretório GraphTutorials denominado Modelos.
Crie um novo arquivo no diretório Modelos chamado ResourceData.cs e adicione o código a seguir.
namespace GraphTutorial.Models { // Class to represent the resourceData object // inside a change notification public class ResourceData { public string Id { get;set; } } }
Crie um novo arquivo no diretório Modelos chamado ChangeNotificationPayload.cs e adicione o código a seguir.
Crie um novo arquivo no diretório Modelos chamado NotificationList.cs e adicione o código a seguir.
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 e substitua todo o conteúdo pelo seguinte.
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}"); } } }
Considere o que o código em Notify.cs faz.
- A
Run
função verifica a presença de um parâmetrovalidationToken
de consulta. Se esse parâmetro estiver presente, ele processa a solicitação como uma solicitação de validação e responde de acordo. - Se a solicitação não for uma solicitação de validação, a carga JSON será desterializada em um
ChangeNotificationCollection
. - Cada notificação na lista é verificada para o valor de estado do cliente esperado e é processada.
- A mensagem que disparou a notificação é recuperada com o Microsoft Graph.
Implementar a função SetSubscription
Nesta seção, você implementará a função SetSubscription. Essa função atuará como uma API chamada pelo aplicativo de teste para criar ou excluir uma assinatura na caixa de entrada de um usuário.
Crie um novo arquivo no diretório Modelos chamado SetSubscriptionPayload.cs e adicione o código a seguir.
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 e substitua todo o conteúdo pelo seguinte.
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(); } } } }
Tome um momento para considerar o que o código em SetSubscription.cs faz.
- A
Run
função lê a carga JSON enviada na solicitação POST para determinar o tipo de solicitação (assinar ou cancelar assinatura), a ID do usuário para assinar e a ID da assinatura para cancelar a assinatura. - Se a solicitação for uma solicitação de assinatura, ela usará o Microsoft Graph SDK para criar uma nova assinatura na caixa de entrada do usuário especificado. A assinatura notificará quando as mensagens são criadas ou atualizadas. A nova assinatura é retornada na carga JSON da resposta.
- Se a solicitação for uma solicitação de cancelamento de assinatura, ela usará o Microsoft Graph SDK para excluir a assinatura especificada.
SetSubscription de chamada do aplicativo de teste
Nesta seção, você implementará funções para criar e excluir assinaturas no aplicativo de teste.
Abra ./TestClient/azurefunctions.js e adicione a seguinte função.
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 }); }
Esse código chama a
SetSubscription
Função do Azure para assinar e adiciona a nova assinatura à matriz de assinaturas na sessão.Adicione a seguinte função 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 chama a
SetSubscription
Função do Azure para cancelar a assinatura e remove a assinatura da matriz de assinaturas na sessão.Se você não tiver ngrok em execução, execute ngrok (
ngrok http 7071
) e copie a URL de encaminhamento HTTPS.Adicione a URL ngrok ao armazenamento de segredos do usuário executando o seguinte comando.
dotnet user-secrets set ngrokUrl "YOUR_NGROK_URL_HERE"
Importante
Se você reiniciar o ngrok, você precisará repetir esse comando para atualizar sua URL ngrok.
Altere o diretório atual em sua CLI para o diretório ./GraphTutorial e execute o seguinte comando para iniciar a Função do Azure localmente.
func start
Atualize o SPA e selecione o item de nav Assinaturas . Insira uma ID de usuário para um usuário em sua Microsoft 365 que tenha uma caixa de correio Exchange Online de usuário. Isso pode ser do usuário
id
(do Microsoft Graph) ou douserPrincipalName
usuário. Clique em Inscrever-se.A página atualiza mostrando a nova assinatura na tabela.
Envie um email para o usuário. Após um breve período, a
Notify
função deve ser chamada. Você pode verificar isso na interface da Web do ngrok (http://localhost:4040
) ou na saída de depuração do projeto função do 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) ...
No aplicativo de teste, clique em Excluir na linha de tabela da assinatura. A página é atualizada e a assinatura não está mais na tabela.
Preparar para publicar no Azure
Neste exercício, você aprenderá sobre quais alterações são necessárias para a função do Azure de exemplo para se preparar para publicação em um aplicativo de Funções do Azure.
Código de atualização
A configuração é lida no armazenamento de segredos do usuário, que só se aplica ao seu computador de desenvolvimento. Antes de publicar no Azure, você precisará alterar onde armazenar sua configuração e atualizar o código em Program.cs de acordo.
Os segredos do aplicativo devem ser armazenados em armazenamento seguro, como o Azure Key Vault.
Atualizar a configuração do CORS para a função Azure
Neste exemplo, configuramos o CORS em local.settings.json para permitir que o aplicativo de teste chame a função. Você precisará configurar sua função publicada para permitir que todos os aplicativos SPA que a chamem.
Atualizar registros de aplicativos
A knownClientApplications
propriedade no manifesto do aplicativo Graph do Azure Function precisará ser atualizada com as IDs do aplicativo de todos os aplicativos que chamarão a Função Azure.
Recriar assinaturas existentes
Todas as assinaturas criadas usando a URL de webhook em seu computador local ou ngrok devem ser recriadas usando a URL de produção da Notify
Função Azure.
Parabéns!
Você concluiu o tutorial do Microsoft Graph Funções do Azure. Agora que você tem um aplicativo de trabalho que chama a Microsoft Graph, você pode experimentar e adicionar novos recursos. Visite a visão geral do microsoft Graph para ver todos os dados que você pode acessar com o Microsoft Graph.
Comentários
Forneça qualquer comentário sobre este tutorial no repositório GitHub.
Tem algum problema com essa seção? Se tiver, envie seus comentários para que possamos melhorar esta seção.