Tutorial: Entrar em usuários e chamar a API do Microsoft Graph em um aplicativo de desktop Electron

Neste tutorial, você cria um aplicativo de desktop Electron que entra em usuários e chama o Microsoft Graph usando o fluxo de código de autorização com PKCE. O aplicativo de área de trabalho que você cria usa a Biblioteca de Autenticação da Microsoft (MSAL) para Node.js.

Siga as etapas neste tutorial para:

  • Registrar o aplicativo no portal do Azure
  • Criar um projeto de aplicativo de desktop Electron
  • Adicionar lógica de autenticação ao seu aplicativo
  • Adicionar um método para chamar uma API da Web
  • Adicionar detalhes de registo da aplicação
  • Testar a aplicação

Pré-requisitos

Registar a candidatura

Primeiro, conclua as etapas em Registrar um aplicativo com a plataforma de identidade da Microsoft para registrar seu aplicativo.

Use as seguintes configurações para o registro do aplicativo:

  • Nome: ElectronDesktopApp (sugerido)
  • Tipos de conta suportados: Contas apenas no meu diretório organizacional (inquilino único)
  • Tipo de plataforma: Aplicações móveis e de ambiente de trabalho
  • URI de redirecionamento: http://localhost

Criar o projeto

Crie uma pasta para hospedar seu aplicativo, por exemplo ElectronDesktopApp.

  1. Primeiro, mude para o diretório do projeto no terminal e, em seguida, execute os seguintes npm comandos:

    npm init -y
    npm install --save @azure/msal-node @microsoft/microsoft-graph-client isomorphic-fetch bootstrap jquery popper.js
    npm install --save-dev electron@20.0.0
    
  2. Em seguida, crie uma pasta chamada App. Dentro dessa pasta, crie um arquivo chamado index.html que servirá como interface do usuário. Adicione o seguinte código lá:

    <!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">
        <meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
        <title>MSAL Node Electron Sample App</title>
    
        <!-- adding Bootstrap 4 for UI components  -->
        <link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
    </head>
    
    <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
            <a class="navbar-brand">Microsoft identity platform</a>
            <div class="btn-group ml-auto dropleft">
                <button type="button" id="signIn" class="btn btn-secondary" aria-expanded="false">
                    Sign in
                </button>
                <button type="button" id="signOut" class="btn btn-success" hidden aria-expanded="false">
                    Sign out
                </button>
            </div>
        </nav>
        <br>
        <h5 class="card-header text-center">Electron sample app calling MS Graph API using MSAL Node</h5>
        <br>
        <div class="row" style="margin:auto">
            <div id="cardDiv" class="col-md-6" style="display:none; margin:auto">
                <div class="card text-center">
                    <div class="card-body">
                        <h5 class="card-title" id="WelcomeMessage">Please sign-in to see your profile and read your mails
                        </h5>
                        <div id="profileDiv"></div>
                        <br>
                        <br>
                        <button class="btn btn-primary" id="seeProfile">See Profile</button>
                    </div>
                </div>
            </div>
        </div>
    
        <!-- importing bootstrap.js and supporting js libraries -->
        <script src="../node_modules/jquery/dist/jquery.js"></script>
        <script src="../node_modules/popper.js/dist/umd/popper.js"></script>
        <script src="../node_modules/bootstrap/dist/js/bootstrap.js"></script>
    
        <!-- importing app scripts | load order is important -->
        <script src="./renderer.js"></script>
    
    </body>
    
    </html>
    
  3. Em seguida, crie o arquivo chamado main.js e adicione o seguinte código:

    /*
     * Copyright (c) Microsoft Corporation. All rights reserved.
     * Licensed under the MIT License.
     */
    
    const path = require("path");
    const { app, ipcMain, BrowserWindow } = require("electron");
    
    const AuthProvider = require("./AuthProvider");
    const { IPC_MESSAGES } = require("./constants");
    const { protectedResources, msalConfig } = require("./authConfig");
    const getGraphClient = require("./graph");
    
    let authProvider;
    let mainWindow;
    
    function createWindow() {
        mainWindow = new BrowserWindow({
            width: 800,
            height: 600,
            webPreferences: { preload: path.join(__dirname, "preload.js") },
        });
    
        authProvider = new AuthProvider(msalConfig);
    }
    
    app.on("ready", () => {
        createWindow();
        mainWindow.loadFile(path.join(__dirname, "./index.html"));
    });
    
    app.on("window-all-closed", () => {
        app.quit();
    });
    
    app.on('activate', () => {
        // On OS X it's common to re-create a window in the app when the
        // dock icon is clicked and there are no other windows open.
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
    
    
    // Event handlers
    ipcMain.on(IPC_MESSAGES.LOGIN, async () => {
        const account = await authProvider.login();
    
        await mainWindow.loadFile(path.join(__dirname, "./index.html"));
        
        mainWindow.webContents.send(IPC_MESSAGES.SHOW_WELCOME_MESSAGE, account);
    });
    
    ipcMain.on(IPC_MESSAGES.LOGOUT, async () => {
        await authProvider.logout();
    
        await mainWindow.loadFile(path.join(__dirname, "./index.html"));
    });
    
    ipcMain.on(IPC_MESSAGES.GET_PROFILE, async () => {
        const tokenRequest = {
            scopes: protectedResources.graphMe.scopes
        };
    
        const tokenResponse = await authProvider.getToken(tokenRequest);
        const account = authProvider.account;
    
        await mainWindow.loadFile(path.join(__dirname, "./index.html"));
    
        const graphResponse = await getGraphClient(tokenResponse.accessToken)
            .api(protectedResources.graphMe.endpoint).get();
    
        mainWindow.webContents.send(IPC_MESSAGES.SHOW_WELCOME_MESSAGE, account);
        mainWindow.webContents.send(IPC_MESSAGES.SET_PROFILE, graphResponse);
    });
    

No trecho de código acima, inicializamos um objeto de janela principal do Electron e criamos alguns manipuladores de eventos para interações com a janela do Electron. Também importamos parâmetros de configuração, instanciamos a classe authProvider para lidar com entrada, saída e aquisição de token e chamamos a API do Microsoft Graph.

  1. Na mesma pasta (App), crie outro arquivo chamado renderizador.js e adicione o seguinte código:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License
    
    /**
     * The renderer API is exposed by the preload script found in the preload.ts
     * file in order to give the renderer access to the Node API in a secure and 
     * controlled way
     */
    const welcomeDiv = document.getElementById('WelcomeMessage');
    const signInButton = document.getElementById('signIn');
    const signOutButton = document.getElementById('signOut');
    const seeProfileButton = document.getElementById('seeProfile');
    const cardDiv = document.getElementById('cardDiv');
    const profileDiv = document.getElementById('profileDiv');
    
    window.renderer.showWelcomeMessage((event, account) => {
        if (!account) return;
    
        cardDiv.style.display = 'initial';
        welcomeDiv.innerHTML = `Welcome ${account.name}`;
        signInButton.hidden = true;
        signOutButton.hidden = false;
    });
    
    window.renderer.handleProfileData((event, graphResponse) => {
        if (!graphResponse) return;
    
        console.log(`Graph API responded at: ${new Date().toString()}`);
        setProfile(graphResponse);
    });
    
    // UI event handlers
    signInButton.addEventListener('click', () => {
        window.renderer.sendLoginMessage();
    });
    
    signOutButton.addEventListener('click', () => {
        window.renderer.sendSignoutMessage();
    });
    
    seeProfileButton.addEventListener('click', () => {
        window.renderer.sendSeeProfileMessage();
    });
    
    const setProfile = (data) => {
        if (!data) return;
        
        profileDiv.innerHTML = '';
    
        const title = document.createElement('p');
        const email = document.createElement('p');
        const phone = document.createElement('p');
        const address = document.createElement('p');
    
        title.innerHTML = '<strong>Title: </strong>' + data.jobTitle;
        email.innerHTML = '<strong>Mail: </strong>' + data.mail;
        phone.innerHTML = '<strong>Phone: </strong>' + data.businessPhones[0];
        address.innerHTML = '<strong>Location: </strong>' + data.officeLocation;
    
        profileDiv.appendChild(title);
        profileDiv.appendChild(email);
        profileDiv.appendChild(phone);
        profileDiv.appendChild(address);
    }
    

Os métodos do renderizador são expostos pelo script de pré-carregamento encontrado no arquivo preload.js para dar ao renderizador acesso ao Node API de forma segura e controlada

  1. Em seguida, crie um novo pré-carregamento de arquivo .js e adicione o seguinte código:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License
    
    const { contextBridge, ipcRenderer } = require('electron');
    
    /**
     * This preload script exposes a "renderer" API to give
     * the Renderer process controlled access to some Node APIs
     * by leveraging IPC channels that have been configured for
     * communication between the Main and Renderer processes.
     */
    contextBridge.exposeInMainWorld('renderer', {
        sendLoginMessage: () => {
            ipcRenderer.send('LOGIN');
        },
        sendSignoutMessage: () => {
            ipcRenderer.send('LOGOUT');
        },
        sendSeeProfileMessage: () => {
            ipcRenderer.send('GET_PROFILE');
        },
        handleProfileData: (func) => {
            ipcRenderer.on('SET_PROFILE', (event, ...args) => func(event, ...args));
        },
        showWelcomeMessage: (func) => {
            ipcRenderer.on('SHOW_WELCOME_MESSAGE', (event, ...args) => func(event, ...args));
        },
    });
    

Esse script de pré-carregamento expõe uma API de renderizador para dar ao processo de renderizador acesso controlado a alguns Node APIs aplicando canais IPC que foram configurados para comunicação entre os processos principal e de renderização.

  1. Finalmente, crie um arquivo chamado constantes.js que armazenará as constantes de cadeias de caracteres para descrever os eventos do aplicativo:

    /*
     * Copyright (c) Microsoft Corporation. All rights reserved.
     * Licensed under the MIT License.
     */
    
    const IPC_MESSAGES = {
        SHOW_WELCOME_MESSAGE: 'SHOW_WELCOME_MESSAGE',
        LOGIN: 'LOGIN',
        LOGOUT: 'LOGOUT',
        GET_PROFILE: 'GET_PROFILE',
        SET_PROFILE: 'SET_PROFILE',
    }
    
    module.exports = {
        IPC_MESSAGES: IPC_MESSAGES,
    }
    

Agora você tem uma GUI simples e interações para seu aplicativo Electron. Depois de concluir o resto do tutorial, a estrutura de arquivos e pastas do seu projeto deve ser semelhante à seguinte:

ElectronDesktopApp/
├── App
│   ├── AuthProvider.js
│   ├── constants.js
│   ├── graph.js
│   ├── index.html
|   ├── main.js
|   ├── preload.js
|   ├── renderer.js
│   ├── authConfig.js
├── package.json

Adicionar lógica de autenticação ao seu aplicativo

Na pasta App , crie um arquivo chamado AuthProvider.js. O arquivo AuthProvider.js conterá uma classe de provedor de autenticação que lidará com login, logout, aquisição de token, seleção de conta e tarefas de autenticação relacionadas usando o MSAL Node. Adicione o seguinte código lá:

/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

const { PublicClientApplication, InteractionRequiredAuthError } = require('@azure/msal-node');
const { shell } = require('electron');

class AuthProvider {
    msalConfig
    clientApplication;
    account;
    cache;

    constructor(msalConfig) {
        /**
         * Initialize a public client application. For more information, visit:
         * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/initialize-public-client-application.md
         */
        this.msalConfig = msalConfig;
        this.clientApplication = new PublicClientApplication(this.msalConfig);
        this.cache = this.clientApplication.getTokenCache();
        this.account = null;
    }

    async login() {
        const authResponse = await this.getToken({
            // If there are scopes that you would like users to consent up front, add them below
            // by default, MSAL will add the OIDC scopes to every token request, so we omit those here
            scopes: [],
        });

        return this.handleResponse(authResponse);
    }

    async logout() {
        if (!this.account) return;

        try {
            /**
             * If you would like to end the session with AAD, use the logout endpoint. You'll need to enable
             * the optional token claim 'login_hint' for this to work as expected. For more information, visit:
             * https://learn.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
             */
            if (this.account.idTokenClaims.hasOwnProperty('login_hint')) {
                await shell.openExternal(`${this.msalConfig.auth.authority}/oauth2/v2.0/logout?logout_hint=${encodeURIComponent(this.account.idTokenClaims.login_hint)}`);
            }

            await this.cache.removeAccount(this.account);
            this.account = null;
        } catch (error) {
            console.log(error);
        }
    }

    async getToken(tokenRequest) {
        let authResponse;
        const account = this.account || (await this.getAccount());

        if (account) {
            tokenRequest.account = account;
            authResponse = await this.getTokenSilent(tokenRequest);
        } else {
            authResponse = await this.getTokenInteractive(tokenRequest);
        }

        return authResponse || null;
    }

    async getTokenSilent(tokenRequest) {
        try {
            return await this.clientApplication.acquireTokenSilent(tokenRequest);
        } catch (error) {
            if (error instanceof InteractionRequiredAuthError) {
                console.log('Silent token acquisition failed, acquiring token interactive');
                return await this.getTokenInteractive(tokenRequest);
            }

            console.log(error);
        }
    }

    async getTokenInteractive(tokenRequest) {
        try {
            const openBrowser = async (url) => {
                await shell.openExternal(url);
            };

            const authResponse = await this.clientApplication.acquireTokenInteractive({
                ...tokenRequest,
                openBrowser,
                successTemplate: '<h1>Successfully signed in!</h1> <p>You can close this window now.</p>',
                errorTemplate: '<h1>Oops! Something went wrong</h1> <p>Check the console for more information.</p>',
            });

            return authResponse;
        } catch (error) {
            throw error;
        }
    }

    /**
     * Handles the response from a popup or redirect. If response is null, will check if we have any accounts and attempt to sign in.
     * @param response
     */
    async handleResponse(response) {
        if (response !== null) {
            this.account = response.account;
        } else {
            this.account = await this.getAccount();
        }

        return this.account;
    }

    /**
     * Calls getAllAccounts and determines the correct account to sign into, currently defaults to first account found in cache.
     * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
     */
    async getAccount() {
        const currentAccounts = await this.cache.getAllAccounts();

        if (!currentAccounts) {
            console.log('No accounts detected');
            return null;
        }

        if (currentAccounts.length > 1) {
            // Add choose account code here
            console.log('Multiple accounts detected, need to add choose account code.');
            return currentAccounts[0];
        } else if (currentAccounts.length === 1) {
            return currentAccounts[0];
        } else {
            return null;
        }
    }
}

module.exports = AuthProvider;

No trecho de código acima, primeiro inicializamos o MSAL Node passando um objeto de PublicClientApplication configuração (msalConfig). Em seguida, expusemos login, logout e getToken métodos a serem chamados pelo módulo principal (principal.js). No login e , adquirimos tokens de ID e getTokenacesso usando a API pública do acquireTokenInteractive Nó MSAL.

Adicionar SDK do Microsoft Graph

Crie um arquivo chamado graph.js. O arquivo graph.js conterá uma instância do Microsoft Graph SDK Client para facilitar o acesso aos dados na API do Microsoft Graph, usando o token de acesso obtido pelo MSAL Node:

const { Client } = require('@microsoft/microsoft-graph-client');
require('isomorphic-fetch');

/**
 * Creating a Graph client instance via options method. For more information, visit:
 * https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md#2-create-with-options
 * @param {String} accessToken
 * @returns
 */
const getGraphClient = (accessToken) => {
    // Initialize Graph client
    const graphClient = Client.init({
        // Use the provided access token to authenticate requests
        authProvider: (done) => {
            done(null, accessToken);
        },
    });

    return graphClient;
};

module.exports = getGraphClient;

Adicionar detalhes de registo da aplicação

Crie um arquivo de ambiente para armazenar os detalhes de registro do aplicativo que serão usados ao adquirir tokens. Para fazer isso, crie um arquivo chamado authConfig.js dentro da pasta raiz do exemplo (ElectronDesktopApp) e adicione o seguinte código:

/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

const { LogLevel } = require("@azure/msal-node");

/**
 * Configuration object to be passed to MSAL instance on creation.
 * For a full list of MSAL.js configuration parameters, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
 */
const AAD_ENDPOINT_HOST = "Enter_the_Cloud_Instance_Id_Here"; // include the trailing slash

const msalConfig = {
    auth: {
        clientId: "Enter_the_Application_Id_Here",
        authority: `${AAD_ENDPOINT_HOST}Enter_the_Tenant_Info_Here`,
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: LogLevel.Verbose,
        },
    },
};

/**
 * Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
 */
const GRAPH_ENDPOINT_HOST = "Enter_the_Graph_Endpoint_Here"; // include the trailing slash

const protectedResources = {
    graphMe: {
        endpoint: `${GRAPH_ENDPOINT_HOST}v1.0/me`,
        scopes: ["User.Read"],
    }
};


module.exports = {
    msalConfig: msalConfig,
    protectedResources: protectedResources,
};

Preencha estes detalhes com os valores que você obtém do portal de registro de aplicativo do Azure:

  • Enter_the_Tenant_Id_here deve ser um dos seguintes:
    • Se o seu aplicativo oferecer suporte a contas nesse diretório organizacional, substitua esse valor pelo ID do Locatário ou Nome do Locatário. Por exemplo, contoso.microsoft.com.
    • Se seu aplicativo oferecer suporte a contas em qualquer diretório organizacional, substitua esse valor por organizations.
    • Se o seu aplicativo oferecer suporte a contas em qualquer diretório organizacional e contas pessoais da Microsoft, substitua esse valor por common.
    • Para restringir o suporte apenas a contas pessoais da Microsoft, substitua este valor por consumers.
  • Enter_the_Application_Id_Here: O ID do aplicativo (cliente) do aplicativo que você registrou.
  • Enter_the_Cloud_Instance_Id_Here: A instância de nuvem do Azure na qual seu aplicativo está registrado.
    • Para a nuvem principal (ou global) do Azure, insira https://login.microsoftonline.com/.
    • Para nuvens nacionais (por exemplo, China), você pode encontrar valores apropriados em Nuvens nacionais.
  • Enter_the_Graph_Endpoint_Here é a instância da API do Microsoft Graph com a qual o aplicativo deve se comunicar.
    • Para o ponto de extremidade global da API do Microsoft Graph, substitua ambas as instâncias dessa cadeia de caracteres por https://graph.microsoft.com/.
    • Para pontos de extremidade em implantações de nuvem nacionais, consulte Implantações de nuvem nacional na documentação do Microsoft Graph.

Testar a aplicação

Você concluiu a criação do aplicativo e agora está pronto para iniciar o aplicativo de desktop Electron e testar a funcionalidade do aplicativo.

  1. Inicie o aplicativo executando o seguinte comando de dentro da raiz da pasta do projeto:
electron App/main.js
  1. Na janela principal do aplicativo, você deve ver o conteúdo do seu arquivo de índice.html e o botão Entrar .

Testar entrada e saída

Depois que o arquivo .html índice for carregado, selecione Entrar. Você será solicitado a entrar com a plataforma de identidade da Microsoft:

sign-in prompt

Se você concordar com as permissões solicitadas, os aplicativos da Web exibirão seu nome de usuário, significando um login bem-sucedido:

successful sign-in

Chamada de API da Web de teste

Depois de entrar, selecione Consulte Perfil para exibir as informações de perfil de usuário retornadas na resposta da chamada para a API do Microsoft Graph. Após o consentimento, você verá as informações do perfil retornadas na resposta:

profile information from Microsoft Graph

Como funciona a aplicação

Quando um usuário seleciona o botão Entrar pela primeira vez, o acquireTokenInteractive método de nó MSAL. Esse método redireciona o usuário para entrar com o ponto de extremidade da plataforma de identidade da Microsoft e valida as credenciais do usuário, obtém um código de autorização e, em seguida, troca esse código por um token de ID, token de acesso e token de atualização. O MSAL Node também armazena esses tokens em cache para uso futuro.

O token de ID contém informações básicas sobre o usuário, como seu nome para exibição. O token de acesso tem uma vida útil limitada e expira após 24 horas. Se você planeja usar esses tokens para acessar o recurso protegido, seu servidor back-end deve validá-lo para garantir que o token foi emitido para um usuário válido para seu aplicativo.

O aplicativo de área de trabalho que você criou neste tutorial faz uma chamada REST para a API do Microsoft Graph usando um token de acesso como token de portador no cabeçalho da solicitação (RFC 6750).

A API do Microsoft Graph requer o escopo user.read para ler o perfil de um usuário. Por padrão, esse escopo é adicionado automaticamente em todos os aplicativos registrados no portal do Azure. Outras APIs para o Microsoft Graph e APIs personalizadas para seu servidor back-end podem exigir escopos extras. Por exemplo, a API do Microsoft Graph requer o escopo Mail.Read para listar o email do usuário.

À medida que você adiciona escopos, os usuários podem ser solicitados a fornecer outro consentimento para os escopos adicionados.

Ajuda e suporte

Se precisar de ajuda, quiser comunicar um problema ou quiser saber mais sobre as suas opções de suporte, consulte Ajuda e suporte para programadores.

Próximos passos

Se você quiser se aprofundar no desenvolvimento de aplicativos de desktop Node.js e Electron na plataforma de identidade da Microsoft, consulte nossa série de cenários com várias partes: