Samouczek: logowanie użytkowników i wywoływanie interfejsu API programu Microsoft Graph w aplikacji klasycznej Electron

W tym samouczku utworzysz aplikację klasyczną Electron, która loguje użytkowników i wywołuje program Microsoft Graph przy użyciu przepływu kodu autoryzacji za pomocą protokołu PKCE. Utworzona aplikacja klasyczna używa biblioteki Microsoft Authentication Library (MSAL) dla środowiska Node.js.

Wykonaj kroki opisane w tym samouczku, aby:

  • Rejestrowanie aplikacji w witrynie Azure Portal
  • Tworzenie projektu aplikacji klasycznej Electron
  • Dodawanie logiki uwierzytelniania do aplikacji
  • Dodawanie metody do wywoływania internetowego interfejsu API
  • Dodawanie szczegółów rejestracji aplikacji
  • Testowanie aplikacji

Wymagania wstępne

Rejestrowanie aplikacji

Najpierw wykonaj kroki opisane w temacie Rejestrowanie aplikacji przy użyciu Platforma tożsamości Microsoft, aby zarejestrować aplikację.

Użyj następujących ustawień rejestracji aplikacji:

  • Nazwa: ElectronDesktopApp (sugerowane)
  • Obsługiwane typy kont: konta tylko w moim katalogu organizacyjnym (tylko jedna dzierżawa)
  • Typ platformy: Aplikacje mobilne i klasyczne
  • Identyfikator URI przekierowania: http://localhost

Tworzenie projektu

Utwórz folder do hostowania aplikacji, na przykład ElectronDesktopApp.

  1. Najpierw przejdź do katalogu projektu w terminalu, a następnie uruchom następujące npm polecenia:

    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. Następnie utwórz folder o nazwie App. W tym folderze utwórz plik o nazwie index.html , który będzie służyć jako interfejs użytkownika. Dodaj tam następujący kod:

    <!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. Następnie utwórz plik o nazwie main.js i dodaj następujący 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);
    });
    

W powyższym fragmencie kodu zainicjujemy główny obiekt okna Electron i utworzymy niektóre procedury obsługi zdarzeń na potrzeby interakcji z oknem Elektron. Importujemy również parametry konfiguracji, tworzymy wystąpienie klasy authProvider do obsługi logowania, wylogowyywania i pozyskiwania tokenów oraz wywoływania interfejsu API programu Microsoft Graph.

  1. W tym samym folderze (Aplikacja) utwórz inny plik o nazwie renderer.js i dodaj następujący 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);
    }
    

Metody modułu renderowania są uwidocznione przez skrypt ładowania wstępnego znajdującego się w pliku preload.js w celu zapewnienia modułowi renderatorowi dostępu do Node API obiektu w bezpieczny i kontrolowany sposób

  1. Następnie utwórz nowy plik preload.js i dodaj następujący 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));
        },
    });
    

Ten skrypt wstępnego ładowania uwidacznia interfejs API modułu renderowania, aby zapewnić kontrolowany dostęp Node APIs do niektórych procesów renderowania przez zastosowanie kanałów IPC skonfigurowanych do komunikacji między procesami głównymi i rendererowymi.

  1. Na koniec utwórz plik o nazwie constants.js, który będzie przechowywać stałe ciągów do opisywania zdarzeń aplikacji:

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

Masz teraz prosty graficzny interfejs użytkownika i interakcje dla aplikacji Electron. Po ukończeniu pozostałej części samouczka struktura plików i folderów projektu powinna wyglądać podobnie do następującej:

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

Dodawanie logiki uwierzytelniania do aplikacji

W folderze App utwórz plik o nazwie AuthProvider.js. Plik AuthProvider.js będzie zawierać klasę dostawcy uwierzytelniania, która będzie obsługiwać logowanie, wylogowywanie, pozyskiwanie tokenów, wybór konta i powiązane zadania uwierzytelniania przy użyciu węzła MSAL. Dodaj tam następujący kod:

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

W powyższym fragmencie kodu najpierw zainicjowaliśmy węzeł PublicClientApplication MSAL, przekazując obiekt konfiguracji (msalConfig). Następnie uwidoczniliśmy loginlogout metody i getToken , które mają być wywoływane przez moduł główny (main.js). W login systemach i getTokenuzyskujemy identyfikatory i tokeny dostępu przy użyciu publicznego interfejsu API biblioteki MSAL Node acquireTokenInteractive .

Dodawanie zestawu Microsoft Graph SDK

Utwórz plik o nazwie graph.js. Plik graph.js będzie zawierać wystąpienie klienta zestawu MICROSOFT Graph SDK w celu ułatwienia uzyskiwania dostępu do danych w interfejsie API programu Microsoft Graph przy użyciu tokenu dostępu uzyskanego przez środowisko 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;

Dodawanie szczegółów rejestracji aplikacji

Utwórz plik środowiska do przechowywania szczegółów rejestracji aplikacji, które będą używane podczas uzyskiwania tokenów. W tym celu utwórz plik o nazwie authConfig.js w folderze głównym przykładu (ElectronDesktopApp) i dodaj następujący 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,
};

Wypełnij te szczegóły wartościami uzyskanymi w portalu rejestracji aplikacji platformy Azure:

  • Enter_the_Tenant_Id_here powinien być jednym z następujących elementów:
    • Jeśli aplikacja obsługuje konta w tym katalogu organizacyjnym, zastąp tę wartość identyfikatorem dzierżawy lub nazwą dzierżawy. Na przykład contoso.microsoft.com.
    • Jeśli aplikacja obsługuje konta w dowolnym katalogu organizacyjnym, zastąp tę wartość wartością organizations.
    • Jeśli aplikacja obsługuje konta w dowolnym katalogu organizacyjnym i osobistych kontach Microsoft, zastąp tę wartość wartością common.
    • Aby ograniczyć obsługę tylko do osobistych kont Microsoft, zastąp tę wartość wartością consumers.
  • Enter_the_Application_Id_Here: identyfikator aplikacji (klienta) zarejestrowanej aplikacji.
  • Enter_the_Cloud_Instance_Id_Here: wystąpienie chmury platformy Azure, w którym zarejestrowano aplikację.
    • W przypadku głównej (lub globalnej) chmury platformy Azure wprowadź .https://login.microsoftonline.com/
    • W przypadku chmur krajowych (na przykład w Chinach) można znaleźć odpowiednie wartości w chmurach krajowych.
  • Enter_the_Graph_Endpoint_Here to wystąpienie interfejsu API programu Microsoft Graph, z którymi aplikacja powinna się komunikować.
    • W przypadku globalnego punktu końcowego interfejsu API programu Microsoft Graph zastąp oba wystąpienia tego ciągu ciągiem https://graph.microsoft.com/.
    • Aby uzyskać informacje o punktach końcowych we wdrożeniach chmury krajowej, zobacz Wdrożenia chmury krajowej w dokumentacji programu Microsoft Graph.

Testowanie aplikacji

Ukończono tworzenie aplikacji i wszystko jest gotowe do uruchomienia aplikacji klasycznej Electron i przetestowania funkcjonalności aplikacji.

  1. Uruchom aplikację, uruchamiając następujące polecenie z poziomu katalogu głównego folderu projektu:
electron App/main.js
  1. W oknie głównym aplikacji powinna zostać wyświetlona zawartość pliku index.html i przycisk Zaloguj się .

Testowanie logowania i wylogowywanie

Po załadowaniu pliku index.html wybierz pozycję Zaloguj. Zostanie wyświetlony monit o zalogowanie się przy użyciu Platforma tożsamości Microsoft:

sign-in prompt

Jeśli wyrazisz zgodę na żądane uprawnienia, aplikacje internetowe wyświetlają nazwę użytkownika, co oznacza pomyślne zalogowanie:

successful sign-in

Testowanie wywołania internetowego interfejsu API

Po zalogowaniu wybierz pozycję Zobacz profil , aby wyświetlić informacje o profilu użytkownika zwrócone w odpowiedzi z wywołania interfejsu API programu Microsoft Graph. Po wyrażeniu zgody wyświetlisz informacje o profilu zwrócone w odpowiedzi:

profile information from Microsoft Graph

Jak działa aplikacja

Gdy użytkownik wybierze przycisk Zaloguj się po raz pierwszy, acquireTokenInteractive metoda biblioteki MSAL Node. Ta metoda przekierowuje użytkownika do logowania się przy użyciu punktu końcowego Platforma tożsamości Microsoft i weryfikuje poświadczenia użytkownika, uzyskuje kod autoryzacji, a następnie wymienia ten kod tokenu identyfikatora, token dostępu i token odświeżania. Biblioteka MSAL Node buforuje również te tokeny do użycia w przyszłości.

Token identyfikatora zawiera podstawowe informacje o użytkowniku, takie jak nazwa wyświetlana. Token dostępu ma ograniczony okres istnienia i wygasa po 24 godzinach. Jeśli planujesz używać tych tokenów do uzyskiwania dostępu do chronionego zasobu, serwer zaplecza musi zweryfikować go, aby zagwarantować, że token został wystawiony dla prawidłowego użytkownika aplikacji.

Aplikacja klasyczna utworzona w tym samouczku wykonuje wywołanie REST do interfejsu API programu Microsoft Graph przy użyciu tokenu dostępu jako tokenu elementu nośnego w nagłówku żądania (RFC 6750).

Interfejs API programu Microsoft Graph wymaga zakresu user.read w celu odczytania profilu użytkownika. Domyślnie ten zakres jest automatycznie dodawany w każdej aplikacji zarejestrowanej w witrynie Azure Portal. Inne interfejsy API dla programu Microsoft Graph i niestandardowe interfejsy API dla serwera zaplecza mogą wymagać dodatkowych zakresów. Na przykład interfejs API programu Microsoft Graph wymaga zakresu Mail.Read , aby wyświetlić listę wiadomości e-mail użytkownika.

Podczas dodawania zakresów użytkownicy mogą być monitowani o udzielenie innej zgody dla dodanych zakresów.

Pomoc i obsługa techniczna 

Jeśli potrzebujesz pomocy, chcesz zgłosić problem lub poznać opcje pomocy technicznej, zobacz Pomoc i obsługa techniczna dla deweloperów.

Następne kroki

Jeśli chcesz dowiedzieć się więcej na temat tworzenia aplikacji klasycznych Node.js i Electron w Platforma tożsamości Microsoft, zobacz naszą wieloczęściową serię scenariuszy: