Zelfstudie: gebruikers aanmelden en de Microsoft Graph API aanroepen in een Electron desktop-app

In deze zelfstudie bouwt u een Electron-desktoptoepassing die gebruikers aanmeldt en Microsoft Graph aanroept met behulp van de autorisatiecodestroom met PKCE. De desktop-app die u bouwt, gebruikt de Microsoft Authentication Library (MSAL) voor Node.js.

Volg de stappen in deze zelfstudie voor het volgende:

  • De app registreren in de Azure Portal
  • Een project voor de Electron-desktop-app maken
  • Verificatielogica toevoegen aan uw app
  • Een methode toevoegen om een web-API aan te roepen
  • Registratiegegevens van app toevoegen
  • De app testen

Vereisten

Registreer de toepassing

Voltooi eerst de stappen in Een toepassing registreren bij het Microsoft identity platform om uw app te registreren.

Gebruik de volgende instellingen voor uw app-registratie:

  • Naam: ElectronDesktopApp (voorgesteld)
  • Ondersteunde accounttypen: alleen accounts in mijn organisatiemap (één tenant)
  • Platformtype: mobiele en desktoptoepassingen
  • URI omleiden: http://localhost

Het project maken

Maak een map voor het hosten van uw toepassing, bijvoorbeeld ElectronDesktopApp.

  1. Ga eerst naar de projectmap in uw terminal en voer de volgende npm-opdrachten uit:

    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. Maak vervolgens een map met de naam App. Maak in deze map een bestand met de naam index.html die als gebruikersinterface zal dienen. Voeg daar de volgende code toe:

    <!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. Maak vervolgens het bestand met de naam main.js en voeg de volgende code toe:

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

In het bovenstaande codefragment initialiseren we een Electron-hoofdvensterobject en maken we enkele gebeurtenis-handlers voor interacties met het Electron-venster. We importeren ook configuratieparameters, instantiëren authProvider-klasse voor het afhandelen van het aanmelden, afmelden en tokenverwerving, en het aanroepen van de Microsoft Graph API.

  1. Maak in dezelfde map (App) een ander bestand met de naam renderer.js en voeg de volgende code toe:

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

De weergavemethoden worden getoond door het script voor vooraf laden dat is gevonden in het bestand preload.js om op een veilige en gecontroleerde manier de weergave toegang te geven tot de Node API

  1. Maak vervolgens een nieuw bestand preload.js en voeg de volgende code toe:

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

Met dit preloadscript wordt een renderer-API weergegeven om het rendererproces gecontroleerde toegang te geven tot sommige Node APIs door IPC-kanalen toe te passen die zijn geconfigureerd voor communicatie tussen de hoofd- en rendererprocessen.

  1. Maak tot slot een bestand met de naam constants.js dat de tekenreeksconstanten zal opslaan voor het beschrijven van de gebeurtenissen van de toepassing:

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

U hebt nu een eenvoudige GUI en interacties voor uw Electron-app. Nadat u de rest van de zelfstudie hebt voltooid, moeten de bestands- en mapstructuur van het project er ongeveer als volgt uitzien:

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

Verificatielogica toevoegen aan uw app

Maak in de map App een bestand met de naam AuthProvider.js. Het bestand AuthProvider.js bevat een verificatieproviderklasse waarmee taken, zoals aanmelden, afmelden, verwerving van tokens, accountselectie en gerelateerde verificatietaken met behulp van het MSAL-knooppunt. Voeg daar de volgende code toe:

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

In het bovenstaande codefragment hebben we eerst het MSAL-knooppunt PublicClientApplication geïnitialiseerd door een configuratieobject (msalConfig) door te geven. We hebben vervolgens methoden login, logout en getToken beschikbaar gesteld die moeten worden aangeroepen door de hoofdmodule (main.js). In login en getTokenverkrijgen we id- en toegangstokens met behulp van de openbare MSAL Node-API acquireTokenInteractive .

Microsoft Graph SDK toevoegen

Maak een bestand met de naam graph.js. Het bestand graph.js bevat een exemplaar van de Microsoft Graph SDK-client om toegang tot gegevens in de Microsoft Graph API te vergemakkelijken met behulp van het toegangstoken dat is verkregen door 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;

Registratiegegevens van app toevoegen

Maak een omgevingsbestand om de app-registratiegegevens op te slaan die worden gebruikt bij het verkrijgen van tokens. Maak hiervoor een bestand met de naam authConfig.js in de hoofdmap van het voorbeeld (ElectronDesktopApp) en voeg de volgende code toe:

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

Vul deze gegevens in met de waarden die u hebt verkregen via de registratie-portal van de Azure-app:

  • Enter_the_Tenant_Id_here moet een van de volgende zijn:
    • Als uw toepassing ondersteuning biedt voor accounts in deze organisatiemap, vervangt u deze waarde door de Tenant-id of Tenantnaam. Bijvoorbeeld contoso.microsoft.com.
    • Als uw toepassing ondersteuning biedt voor accounts in elke organisatiemap, vervangt u waarde door organizations.
    • Als uw toepassing accounts in elke organisatiemap en persoonlijke Microsoft-accounts ondersteunt, vervang deze waarde dan door common.
    • Als u de ondersteuning wilt beperken tot alleen persoonlijke Microsoft-accounts, vervang deze waarde dan door consumers.
  • Enter_the_Application_Id_Here: de toepassings-id (client) van de toepassing die u hebt geregistreerd.
  • Enter_the_Cloud_Instance_Id_Here: Het Azure-cloudexemplaren waarin uw toepassing is geregistreerd.
    • Voer https://login.microsoftonline.com/ in voor de hoofd- (of globale) Azure-cloud.
    • Voor nationale clouds (bijvoorbeeld China) kunt u de juiste waarden vinden in Nationale clouds.
  • Enter_the_Graph_Endpoint_Here is het exemplaar van de Microsoft Graph API waarmee de toepassing moet communiceren.
    • Vervang beide exemplaren van deze teken reeks door https://graph.microsoft.com/ voor het wereldwijde Microsoft Graph API-eindpunt.
    • Zie Nationale cloudimplementaties in de Microsoft Graph-documentatie voor eindpunten in nationale cloudimplementaties.

De app testen

De toepassing is aangemaakt en u ben nu klaar om de Electron desktop-app te starten en de functionaliteit van de app te testen.

  1. Start de app door de volgende opdracht uit te voeren in de hoofdmap van de projectmap:
electron App/main.js
  1. In het hoofdvenster van de toepassing, zou u de inhoud van het bestand index.html en de knop Aanmelden moeten zien.

Aanmelden en afmelden testen

Nadat het bestand index.html is geladen, selecteert u Aanmelden. U wordt gevraagd u aan te melden bij het Microsoft identity platform:

sign-in prompt

Als u akkoord gaat met de aangevraagde machtigingen, wordt in de webtoepassingen uw gebruikersnaam weer gegeven, met een geslaagde aanmelding:

successful sign-in

Web-API-aanroep testen

Nadat u zich hebt aangemeld, selecteert u Profiel weergeven om de gebruikersprofielgegevens weer te geven die zijn geretourneerd in het antwoord van de aanroep naar de Microsoft Graph API. Na toestemming bekijkt u de profielgegevens die in het antwoord worden geretourneerd:

profile information from Microsoft Graph

Werking van de toepassing

Wanneer een gebruiker de knop Aanmelden voor de eerste keer selecteert, wordt de acquireTokenInteractive methode MSAL Node gebruikt. Met deze methode wordt de gebruiker omgeleid om zich aan te melden met het Microsoft Identity Platform-eindpunt en de referenties van de gebruiker te valideren, een autorisatiecode te verkrijgen en die code vervolgens uit te wisselen voor een id-token, toegangstoken en vernieuwingstoken. MSAL Node slaat deze tokens ook op in de cache voor toekomstig gebruik.

Het ID-token bevat basisinformatie over de gebruiker, zoals de weergavenaam. Het toegangstoken heeft een beperkte levensduur en verloopt na 24 uur. Als u van plan bent deze tokens te gebruiken om toegang te krijgen tot de beschermde bron, moet uw back-endserver dit valideren om te garanderen dat het token is uitgegeven aan een geldige gebruiker voor uw toepassing.

De desktop-app die u in deze zelfstudie hebt gemaakt, voert een REST-aanroep uit naar de Microsoft Graph API met behulp van een toegangstoken als bearer-token in de aanvraagheader (RFC 6750).

De Microsoft Graph API vereist het bereik user.read om het profiel van een gebruiker te lezen. Dit bereik wordt standaard automatisch toegevoegd in elke toepassing die is geregistreerd in de Azure-portal. Overige API's voor Microsoft Graph en aangepaste API's voor uw back-endserver vereisen mogelijk extra bereiken. De Microsoft Graph API vereist bijvoorbeeld het bereik Mail.Read om de e-mail van de gebruiker op te sommen.

Wanneer u bereiken toevoegt, kunnen uw gebruikers worden gevraagd nog eens hun toestemming te geven voor de toegevoegde bereiken.

Help en ondersteuning

Als u hulp nodig hebt, een probleem wilt melden of meer informatie wilt over uw ondersteuningsopties, raadpleegt u Hulp en ondersteuning voor ontwikkelaars.

Volgende stappen

Als u meer wilt weten over de ontwikkeling van Node.js en de Electron-desktoptoepassing op het Microsoft identity platform, kunt onze meerdelige reeks scenario's bekijken: