Kurz: Přihlášení uživatelů a volání rozhraní Microsoft Graph API v desktopové aplikaci Elektron

V tomto kurzu vytvoříte desktopovou aplikaci Elektron, která přihlašuje uživatele a volá Microsoft Graph pomocí toku autorizačního kódu s PKCE. Desktopová aplikace, kterou vytvoříte, používá knihovnu Microsoft Authentication Library (MSAL) pro Node.js.

Postupujte podle kroků v tomto kurzu:

  • Registrace aplikace na webu Azure Portal
  • Vytvoření projektu desktopové aplikace Elektron
  • Přidání logiky ověřování do aplikace
  • Přidání metody pro volání webového rozhraní API
  • Přidání podrobností o registraci aplikace
  • Otestování aplikace

Požadavky

Registrace aplikace

Nejprve proveďte kroky v části Registrace aplikace na platformě Microsoft Identity Platform a zaregistrujte aplikaci.

Pro registraci aplikace použijte následující nastavení:

  • Název: ElectronDesktopApp (navrhované)
  • Podporované typy účtů: Účty pouze v adresáři organizace (jeden tenant)
  • Typ platformy: Mobilní a desktopové aplikace
  • Identifikátor URI přesměrování: http://localhost

Vytvoření projektu

Vytvořte složku pro hostování aplikace, například ElectronDesktopApp.

  1. Nejprve přejděte do adresáře projektu v terminálu a spusťte následující npm příkazy:

    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. Pak vytvořte složku s názvem Aplikace. V této složce vytvořte soubor s názvem index.html , který bude sloužit jako uživatelské rozhraní. Přidejte do ní následující kód:

    <!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. Dále vytvořte soubor s názvem main.js a přidejte následující kód:

    /*
     * 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);
    });
    

Ve výše uvedeném fragmentu kódu inicializujeme objekt hlavního okna Elektron a vytvoříme některé obslužné rutiny událostí pro interakce s oknem Elektron. Importujeme také parametry konfigurace, vytvoříme instanci třídy authProvider pro zpracování přihlášení, odhlášení a získání tokenu a zavoláme rozhraní Microsoft Graph API.

  1. Ve stejné složce (App) vytvořte další soubor s názvem renderer.js a přidejte následující kód:

    // 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);
    }
    

Metody rendereru jsou vystaveny skriptem předběžného načtení nalezeného v souboru preload.js , aby renderer získal přístup k Node API zabezpečenému a řízenému způsobu.

  1. Pak vytvořte nový soubor preload.js a přidejte následující kód:

    // 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));
        },
    });
    

Tento skript předběžného načtení zpřístupňuje rozhraní API rendereru, které poskytuje procesům rendereru řízený přístup k některým Node APIs použitím kanálů IPC nakonfigurovaných pro komunikaci mezi hlavními a vykreslovacími procesy.

  1. Nakonec vytvořte soubor s názvem constants.js, který uloží konstanty řetězců pro popis událostí aplikace:

    /*
     * 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,
    }
    

Teď máte jednoduché grafické uživatelské rozhraní a interakce pro aplikaci Elektron. Po dokončení zbývající části kurzu by struktura souborů a složek projektu měla vypadat nějak takto:

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

Přidání logiky ověřování do aplikace

Ve složce Aplikace vytvořte soubor s názvem AuthProvider.js. Soubor AuthProvider.js bude obsahovat třídu zprostředkovatele ověřování, která bude zpracovávat přihlášení, odhlášení, získání tokenu, výběr účtu a související úlohy ověřování pomocí MSAL Node. Přidejte do ní následující kód:

/*
 * 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;

Ve výše uvedeném fragmentu kódu jsme nejprve inicializovali uzel PublicClientApplication MSAL předáním objektu konfigurace (msalConfig). Pak jsme odhalili logina getToken metody, logout které se mají volat hlavním modulem (main.js). V login a getToken, získáme ID a přístupové tokeny pomocí veřejného rozhraní API MSAL Node acquireTokenInteractive .

Přidání sady Microsoft Graph SDK

Vytvořte soubor s názvem graph.js. Soubor graph.js bude obsahovat instanci klienta sady Microsoft Graph SDK pro usnadnění přístupu k datům v rozhraní Microsoft Graph API pomocí přístupového tokenu získaného uzlem MSAL:

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;

Přidání podrobností o registraci aplikace

Vytvořte soubor prostředí pro uložení podrobností o registraci aplikace, které se použijí při získávání tokenů. Uděláte to tak, že vytvoříte soubor s názvem authConfig.js v kořenové složce ukázky (ElectronDesktopApp) a přidáte následující kód:

/*
 * 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,
};

Vyplňte tyto podrobnosti hodnotami, které získáte z portálu pro registraci aplikací Azure:

  • Enter_the_Tenant_Id_here by měla být jedna z následujících možností:
    • Pokud vaše aplikace podporuje účty v tomto organizačním adresáři, nahraďte tuto hodnotu ID tenanta nebo názvem tenanta. Například contoso.microsoft.com.
    • Pokud vaše aplikace podporuje účty v libovolném organizačním adresáři, nahraďte tuto hodnotu organizationshodnotou .
    • Pokud vaše aplikace podporuje účty v libovolném organizačním adresáři a osobních účtech Microsoft, nahraďte tuto hodnotu commonhodnotou .
    • Chcete-li omezit podporu pouze na osobní účty Microsoft, nahraďte tuto hodnotu consumershodnotou .
  • Enter_the_Application_Id_Here: ID aplikace (klienta) aplikace, kterou jste zaregistrovali.
  • Enter_the_Cloud_Instance_Id_Here: Cloudová instance Azure, ve které je vaše aplikace zaregistrovaná.
    • V případě hlavního (nebo globálního) cloudu Azure zadejte https://login.microsoftonline.com/.
    • Pro národní cloudy (například Čína) najdete odpovídající hodnoty v národních cloudech.
  • Enter_the_Graph_Endpoint_Here je instance rozhraní Microsoft Graph API, se kterým by aplikace měla komunikovat.

Otestování aplikace

Dokončili jste vytváření aplikace a jste připraveni spustit desktopovou aplikaci Electron a otestovat její funkčnost.

  1. Spusťte aplikaci spuštěním následujícího příkazu v kořenové složce projektu:
electron App/main.js
  1. V hlavním okně aplikace byste měli vidět obsah souboru index.html a tlačítko Přihlásit se.

Otestování přihlášení a odhlášení

Po načtení souboru index.html vyberte Přihlásit se. Zobrazí se výzva k přihlášení pomocí platformy Microsoft Identity Platform:

sign-in prompt

Pokud souhlasíte s požadovanými oprávněními, webové aplikace zobrazí vaše uživatelské jméno a podepisují úspěšné přihlášení:

successful sign-in

Testování volání webového rozhraní API

Po přihlášení vyberte Zobrazit profil a zobrazte informace o profilu uživatele vrácené v odpovědi z volání rozhraní Microsoft Graph API. Po vyjádření souhlasu zobrazíte informace o profilu vrácené v odpovědi:

profile information from Microsoft Graph

Jak aplikace funguje

Když uživatel poprvé vybere tlačítko Přihlásit se, acquireTokenInteractive metoda uzlu MSAL. Tato metoda přesměruje uživatele na přihlášení pomocí koncového bodu Microsoft Identity Platform a ověří přihlašovací údaje uživatele, získá autorizační kód a pak tento kód pro token ID, přístupový token a obnovovací token. Uzel MSAL tyto tokeny také ukládá do mezipaměti pro budoucí použití.

Token ID obsahuje základní informace o uživateli, například zobrazované jméno. Přístupový token má omezenou životnost a vyprší po 24 hodinách. Pokud plánujete používat tyto tokeny pro přístup k chráněnému prostředku, back-endový server ho musí ověřit, aby se zajistilo, že token byl vydán platnému uživateli pro vaši aplikaci.

Desktopová aplikace, kterou jste vytvořili v tomto kurzu, volá rozhraní MICROSOFT Graph API pomocí přístupového tokenu jako nosný token v hlavičce požadavku (RFC 6750).

Rozhraní Microsoft Graph API vyžaduje obor user.read ke čtení profilu uživatele. Ve výchozím nastavení se tento obor automaticky přidá do každé aplikace zaregistrované na webu Azure Portal. Další rozhraní API pro Microsoft Graph a vlastní rozhraní API pro váš back-endový server můžou vyžadovat další obory. Rozhraní Microsoft Graph API například vyžaduje obor Mail.Read , aby bylo možné zobrazit seznam e-mailů uživatele.

Při přidávání oborů se uživatelům může zobrazit výzva k poskytnutí dalšího souhlasu s přidanými obory.

Nápověda a podpora

Pokud potřebujete pomoc, chcete nahlásit problém nebo se chcete dozvědět o možnostech podpory, přečtěte si nápovědu a podporu pro vývojáře.

Další kroky

Pokud se chcete podrobněji podívat na vývoj desktopových aplikací Node.js a Electron na platformě Microsoft Identity Platform, podívejte se na naši sérii scénářů s více částmi: