Share via


Självstudie: Logga in användare och anropa Microsoft Graph API i en Electron-skrivbordsapp

I den här självstudien skapar du ett Elektron-skrivbordsprogram som loggar in användare och anropar Microsoft Graph med hjälp av auktoriseringskodflödet med PKCE. Skrivbordsappen som du skapar använder Microsoft Authentication Library (MSAL) för Node.js.

Följ stegen i den här självstudien för att:

  • Registrera programmet i Azure-portalen
  • Skapa ett Electron Desktop-appprojekt
  • Lägga till autentiseringslogik i din app
  • Lägga till en metod för att anropa ett webb-API
  • Lägga till appregistreringsinformation
  • Testa appen

Förutsättningar

Registrera programmet

Slutför först stegen i Registrera ett program med Microsofts identitetsplattform för att registrera din app.

Använd följande inställningar för din appregistrering:

  • Namn: ElectronDesktopApp (föreslås)
  • Kontotyper som stöds: Endast konton i min organisationskatalog (enskild klientorganisation)
  • Plattformstyp: Mobil- och skrivbordsprogram
  • Omdirigerings-URI: http://localhost

Skapa projektet

Skapa en mapp som värd för ditt program, till exempel ElectronDesktopApp.

  1. Ändra först till projektkatalogen i terminalen och kör sedan följande npm kommandon:

    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. Skapa sedan en mapp med namnet App. I den här mappen skapar du en fil med namnet index.html som fungerar som användargränssnitt. Lägg till följande kod där:

    <!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. Skapa sedan en fil med namnet main.js och lägg till följande kod:

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

I kodfragmentet ovan initierar vi ett Electron-huvudfönsterobjekt och skapar några händelsehanterare för interaktioner med elektronfönstret. Vi importerar även konfigurationsparametrar, instansierar klassen authProvider för hantering av inloggning, utloggning och tokenförvärv och anropar Microsoft Graph API.

  1. I samma mapp (app) skapar du en annan fil med namnet renderer.js och lägger till följande kod:

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

Återgivningsmetoderna exponeras av förinläsningsskriptet som finns i preload.js-filen för att ge renderaren åtkomst till Node API på ett säkert och kontrollerat sätt

  1. Skapa sedan en ny fil preload.js och lägg till följande kod:

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

Det här förinläsningsskriptet exponerar ett renderar-API för att ge återgivningsprocessen kontrollerad åtkomst till vissa Node APIs genom att tillämpa IPC-kanaler som har konfigurerats för kommunikation mellan huvud- och återgivningsprocesserna.

  1. Skapa slutligen en fil med namnet constants.js som lagrar strängarnas konstanter för att beskriva programhändelserna:

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

Nu har du ett enkelt GUI och interaktioner för din Electron-app. När du har slutfört resten av självstudien bör projektets fil- och mappstruktur se ut ungefär så här:

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

Lägga till autentiseringslogik i din app

Skapa en fil med namnet AuthProvider.js i mappen App. Filen AuthProvider.js innehåller en autentiseringsproviderklass som hanterar inloggning, utloggning, tokenförvärv, val av konto och relaterade autentiseringsuppgifter med MSAL Node. Lägg till följande kod där:

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

I kodfragmentet ovan initierade vi först MSAL Node PublicClientApplication genom att skicka ett konfigurationsobjekt (msalConfig). Sedan exponerade loginvi , logout och getToken metoder som skulle anropas av huvudmodulen (main.js). I login och getTokenhämtar vi ID och åtkomsttoken med msal node acquireTokenInteractive public API.

Lägga till Microsoft Graph SDK

Skapa en fil med namnet graph.js. Filen graph.js innehåller en instans av Microsoft Graph SDK-klienten för att underlätta åtkomsten till data i Microsoft Graph-API:et med hjälp av den åtkomsttoken som hämtas av 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;

Lägga till appregistreringsinformation

Skapa en miljöfil för att lagra den appregistreringsinformation som ska användas vid anskaffning av token. Det gör du genom att skapa en fil med namnet authConfig.js i rotmappen i exemplet (ElectronDesktopApp) och lägga till följande kod:

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

Fyll i den här informationen med de värden som du får från Azure-appregistreringsportalen:

  • Enter_the_Tenant_Id_here bör vara något av följande:
    • Om ditt program stöder konton i den här organisationskatalogen ersätter du det här värdet med klientorganisations-ID:t eller klientorganisationens namn. Exempel: contoso.microsoft.com
    • Om ditt program stöder konton i en organisationskatalog ersätter du det här värdet med organizations.
    • Om ditt program stöder konton i en organisationskatalog och personliga Microsoft-konton ersätter du det här värdet med common.
    • Om du bara vill begränsa stödet till personliga Microsoft-konton ersätter du det här värdet med consumers.
  • Enter_the_Application_Id_Here: Program-ID :t (klient) för det program som du registrerade.
  • Enter_the_Cloud_Instance_Id_Here: Azure-molninstansen där ditt program är registrerat.
    • För det huvudsakliga (eller globala) Azure-molnet anger du https://login.microsoftonline.com/.
    • För nationella moln (till exempel Kina) kan du hitta lämpliga värden i nationella moln.
  • Enter_the_Graph_Endpoint_Here är instansen av Microsoft Graph API som programmet ska kommunicera med.
    • För den globala Microsoft Graph API-slutpunkten ersätter du båda instanserna av den här strängen med https://graph.microsoft.com/.
    • Slutpunkter i nationella molndistributioner finns i Nationella molndistributioner i Microsoft Graph-dokumentationen.

Testa appen

Du har slutfört skapandet av programmet och är nu redo att starta Electron Desktop-appen och testa appens funktioner.

  1. Starta appen genom att köra följande kommando från roten i projektmappen:
electron App/main.js
  1. I programmets huvudfönster bör du se innehållet i filen index.html och knappen Logga in .

Testa inloggning och utloggning

När filen index.html har lästs in väljer du Logga in. Du uppmanas att logga in med Microsofts identitetsplattform:

sign-in prompt

Om du godkänner de begärda behörigheterna visar webbprogrammen ditt användarnamn, vilket betecknar en lyckad inloggning:

successful sign-in

Testa webb-API-anrop

När du har loggat in väljer du Se profil för att visa användarprofilinformationen som returnerades i svaret från anropet till Microsoft Graph-API:et. Efter medgivande visar du profilinformationen som returneras i svaret:

profile information from Microsoft Graph

Så här fungerar programmet

När en användare väljer knappen Logga in för första gången, acquireTokenInteractive metoden för MSAL Node. Den här metoden omdirigerar användaren till inloggning med Microsofts identitetsplattform-slutpunkten och verifierar användarens autentiseringsuppgifter, hämtar en auktoriseringskod och byter sedan koden mot en ID-token, åtkomsttoken och uppdateringstoken. MSAL Node cachelagrar även dessa token för framtida användning.

ID-token innehåller grundläggande information om användaren, till exempel deras visningsnamn. Åtkomsttoken har en begränsad livslängd och upphör att gälla efter 24 timmar. Om du planerar att använda dessa token för åtkomst till en skyddad resurs måste serverdelsservern verifiera den för att garantera att token har utfärdats till en giltig användare för ditt program.

Skrivbordsappen som du har skapat i den här självstudien gör ett REST-anrop till Microsoft Graph API med hjälp av en åtkomsttoken som ägartoken i begärandehuvudet (RFC 6750).

Microsoft Graph API kräver user.read-omfånget för att läsa en användares profil. Som standard läggs det här omfånget automatiskt till i varje program som är registrerat i Azure-portalen. Andra API:er för Microsoft Graph och anpassade API:er för serverdelsservern kan kräva extra omfång. Till exempel kräver Microsoft Graph API-omfånget Mail.Read för att kunna visa användarens e-post.

När du lägger till omfång kan användarna uppmanas att ge ett annat medgivande för de tillagda omfången.

Hjälp och support

Om du behöver hjälp, vill rapportera ett problem eller vill lära dig mer om dina supportalternativ kan du läsa Hjälp och support för utvecklare.

Nästa steg

Om du vill fördjupa dig i utveckling av Node.js- och Electron Desktop-program på Microsofts identitetsplattform kan du läsa vår scenarioserie i flera delar: