Tutorial: Conectar usuários e chamar a API do Microsoft Graph em um aplicativo da área de trabalho do Electron

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

Siga as etapas deste tutorial para:

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

Pré-requisitos

Registrar o aplicativo

Primeiro, conclua as etapas descritas 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 contas com suporte: Somente as contas no meu diretório organizacional (locatário único)
  • Tipo de plataforma: Aplicativos móveis e para desktop
  • URI de redirecionamento: http://localhost

Criar o projeto

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

  1. Primeiro, altere o diretório do projeto em seu terminal e execute os comandos seguintes comandos npm:

    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. Depois, crie uma pasta chamada Aplicativo. Dentro dessa pasta, crie um arquivo chamado index.html que servirá como a interface do usuário. Adicione o seguinte código a ele:

    <!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 um 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 snippet 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, criamos uma instância da classe authProvider para processar a entrada, a saída e a aquisição de token e chamamos a API do Microsoft Graph.

  1. Na mesma pasta (Aplicativo), crie outro arquivo chamado renderer.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 conceder ao renderizador o acesso à Node API de maneira segura e controlada

  1. Depois, crie um novo arquivo preload.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 acesso controlado ao processo do renderizador a algumas Node APIs por meio da aplicação de canais IPC que foram configurados para a comunicação entre os processos principais e do renderizador.

  1. Por fim, crie um arquivo chamado constants.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 do Electron. Depois de concluir o restante do tutorial, a estrutura de arquivos e pastas de 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 a lógica de autenticação ao aplicativo

Na pasta Aplicativo, crie um arquivo chamado AuthProvider.js. O arquivo AuthProvider.js conterá uma classe de provedor de autenticação que processará o logon, o logoff, a aquisição de token, a seleção de conta e as tarefas de autenticação relacionadas usando a MSAL Node. Adicione o seguinte código a ele:

/*
 * 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://video2.skills-academy.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 snippet de código acima, primeiro, inicializamos o PublicClientApplication da MSAL Node transmitindo um objeto de configuração (msalConfig). Depois, expomos os métodos login, logout e getToken a serem chamados pelo módulo principal (main.js). Em login e getToken, adquirimos tokens de ID e de acesso usando a API pública acquireTokenInteractive da MSAL Node.

Adicionar o SDK do Microsoft Graph

Crie um arquivo chamado graph.js. O arquivo graph.js conterá uma instância do Cliente do SDK do Microsoft Graph para facilitar o acesso a dados na API do Microsoft Graph, usando o token de acesso obtido pela 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 registro do aplicativo

Crie um arquivo de ambiente para armazenar os detalhes de registro do aplicativo que serão usados ao adquirir tokens. Para isso, crie um arquivo chamado authConfig.js na 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 esses detalhes com os valores obtidos do portal de registro de aplicativo do Azure:

  • Enter_the_Tenant_Id_here deve ser uma destas opções:
    • Se o aplicativo tem suporte para contas neste diretório organizacional, substitua esse valor pela ID do locatário ou pelo Nome do locatário. Por exemplo, contoso.microsoft.com.
    • Se o aplicativo tem suporte para contas em qualquer diretório organizacional, substitua esse valor por organizations.
    • Se o seu aplicativo tem suporte para contas em qualquer diretório organizacional e contas pessoais Microsoft, substitua esse valor por common.
    • Para restringir o suporte a contas pessoais da Microsoft, substitua esse valor por consumers.
  • Enter_the_Application_Id_Here: a ID do Aplicativo (cliente) do aplicativo que você registrou.
  • Enter_the_Cloud_Instance_Id_Here: a instância de nuvem do Azure na qual o 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 os 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 as duas instâncias dessa cadeia de caracteres por https://graph.microsoft.com/.
    • Para pontos de extremidade em implantações de nuvens nacionais, confira Implantações de nuvens nacionais na documentação do Microsoft Graph.

Testar o aplicativo

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

  1. Inicie o aplicativo executando o seguinte comando na raiz da pasta do projeto:
electron App/main.js
  1. Na janela principal do aplicativo, você verá o conteúdo do arquivo index.html e o botão Entrar.

Testar a entrada e a saída

Depois que o arquivo index.html for carregado, escolha Entrar. Você deverá entrar na plataforma de identidade da Microsoft:

prompt de entrada

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

entrada bem-sucedida

Testar a chamada à API Web

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

informações de perfil do Microsoft Graph

Como o aplicativo funciona

Quando um usuário seleciona o botão Entrar pela primeira vez, o método acquireTokenInteractive da MSAL Node. 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 troca esse código por um token de ID, um token de acesso e um token de atualização. A 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 o nome de exibição dele. O token de acesso tem um tempo de vida limitado e expira após 24 horas. Se você pretende usar esses tokens para acessar um recurso protegido, o servidor back-end precisa validá-los a fim de garantir que o token foi emitido para um usuário válido para o seu aplicativo.

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

A API do Microsoft Graph requer o escopo user.read para ler o perfil do usuário. Por padrão, este escopo é adicionado automaticamente a cada aplicativo registrado no portal do Azure. Outras APIs do Microsoft Graph e APIs personalizadas do servidor de 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 relatar um problema ou desejar saber mais sobre as opções de suporte, confira Ajuda e suporte para desenvolvedores.

Próximas etapas

Caso deseje se aprofundar no desenvolvimento de aplicativos da área de trabalho Node.js e Electron na plataforma de identidade da Microsoft, confira nossa série de cenários de várias partes: