Tutoriel : Connecter des utilisateurs et appeler l’API Microsoft Graph dans une application de bureau Electron

Dans ce tutoriel, vous créez une application de bureau Electron qui connecte les utilisateurs et appelle Microsoft Graph en utilisant le flux du code d’autorisation avec PKCE. L’application de bureau que vous créez utilise la bibliothèque d’authentification Microsoft (MSAL) pour Node.js.

Suivez les étapes de ce tutoriel pour :

  • Inscrire l’application dans le Portail Azure
  • Créer un projet d’application de bureau Electron
  • Ajouter une logique d’authentification à votre application
  • Ajouter une méthode pour appeler une API web
  • Ajouter les détails de l’inscription de l’application
  • Test de l'application

Prérequis

Enregistrement de l’application

Effectuez d’abord les étapes décrites dans Inscrire une application auprès de la plateforme d’identités Microsoft pour enregistrer votre application.

Utilisez les paramètres suivants pour l’inscription de votre application :

  • Nom : ElectronDesktopApp (suggéré)
  • Types de comptes pris en charge : Comptes dans cet annuaire d’organisation uniquement (locataire unique)
  • Type de plateforme : Applications de bureau et mobiles
  • URI de redirection : http://localhost

Créer le projet

Créez un dossier pour héberger votre application, par exemple ElectronDesktopApp.

  1. Tout d’abord, accédez à votre répertoire de projet dans le terminal, puis exécutez les commandes npm suivantes :

    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. Ensuite, créez un dossier nommé App. À l’intérieur de ce dossier, créez un fichier nommé index.html qui servira d’interface utilisateur. Ajoutez-y le code suivant :

    <!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. Ensuite, créez un fichier nommé main.js et ajoutez-y le code suivant :

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

Dans l’extrait de code ci-dessus, nous initialisons un objet de fenêtre principale Electron et créons des gestionnaires d’événements pour les interactions avec la fenêtre Electron. Nous importons également des paramètres de configuration, instancions la classe authProvider pour la gestion de la connexion, de la déconnexion et de l’acquisition de jetons, et appelons l’API Microsoft Graph.

  1. Dans le même dossier (App), créez un autre fichier nommé renderer.js et ajoutez le code suivant :

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

Les méthodes de renderer sont exposées par le script de préchargement trouvé dans le fichier preload.js afin de donner au renderer l’accès à Node API de manière sécurisée et contrôlée

  1. Ensuite, créez un fichier preload.js et ajoutez le code suivant :

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

Ce script de préchargement expose des API de renderer pour donner au processus de renderer un accès contrôlé à certaines Node APIs en appliquant des canaux IPC configurés pour la communication entre les processus principaux et de renderer.

  1. Pour finir, créez un fichier nommé constants.js, qui stockera les constantes de chaînes permettant de décrire les événements d’application :

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

Vous disposez à présent d’une interface graphique utilisateur simple, et d’interactions pour votre application Electron. Une fois le tutoriel terminé, la structure des fichiers et des dossiers de votre projet doit ressembler à ce qui suit :

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

Ajouter une logique d’authentification à votre application

Dans le dossier App, créez un fichier nommé AuthProvider.js. Le fichier AuthProvider.js contiendra une classe de fournisseur d’authentification, prévue pour gérer la connexion, la déconnexion, l’acquisition de jetons, la sélection de compte et les tâches d’authentification associées, à l’aide de MSAL Node. Ajoutez-y le code suivant :

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

Dans l’extrait de code ci-dessus, nous avons d’abord initialisé PublicClientApplication MSAL Node en passant un objet de configuration (msalConfig). Nous avons ensuite exposé les méthodes login, logout et getToken pour qu’elles soient appelées par le module principal (main.js). Dans login et getToken, nous acquérons des jetons d’identification et d’accès en utilisant l’API publique acquireTokenInteractive MSAL Node.

Ajouter le SDK Microsoft Graph

Créez un fichier nommé graph.js. Le fichier graph.js contient une instance du client du SDK Microsoft Graph pour faciliter l’accès aux données sur l’API Microsoft Graph, à l’aide du jeton d’accès obtenu par le nœud 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;

Ajouter les détails de l’inscription de l’application

Créez un fichier d’environnement pour stocker les détails de l’inscription de l’application qui seront utilisés lors de l’acquisition de jetons. Pour ce faire, créez un fichier nommé authConfig.js dans le dossier racine de l’exemple (ElectronDesktopApp) et ajoutez le code suivant :

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

Utilisez les valeurs que vous obtenez du portail d’inscription des applications Azure pour renseigner ces détails :

  • Enter_the_Tenant_Id_here doit prendre l’une des valeurs suivantes :
    • Si votre application prend en charge les comptes dans cet annuaire organisationnel, remplacez cette valeur par l’ID de locataire ou le Nom du locataire. Par exemple : contoso.microsoft.com.
    • Si votre application prend en charge les comptes dans un annuaire organisationnel, remplacez cette valeur par organizations.
    • Si votre application prend en charge les comptes dans un annuaire organisationnel et comptes personnels Microsoft, remplacez cette valeur par common.
    • Pour limiter la prise en charge aux comptes Microsoft personnels uniquement, remplacez cette valeur par consumers.
  • Enter_the_Application_Id_Here: ID d’application (client) de l’application que vous avez inscrite.
  • Enter_the_Cloud_Instance_Id_Here: instance cloud Azure dans laquelle votre application est inscrite.
    • Pour le cloud Azure principal (ou mondial), entrez https://login.microsoftonline.com/.
    • Pour les clouds nationaux (par exemple la Chine), vous trouverez les valeurs appropriées dans Clouds nationaux.
  • Enter_the_Graph_Endpoint_Here est l’instance de l’API Microsoft Graph avec laquelle l’application doit communiquer.
    • Pour le point de terminaison de l’API Microsoft Graph mondial, remplacez les deux instances de cette chaîne par https://graph.microsoft.com/.
    • Pour les points de terminaison dans les déploiements dans des clouds nationaux, consultez Déploiements dans des clouds nationaux dans la documentation Microsoft Graph.

Test de l'application

Vous venez de terminer la création de l’application et êtes maintenant prêt à lancer l’application de bureau Electron pour tester les fonctionnalités de l’application.

  1. Démarrez l’application en exécutant la commande suivante à la racine du dossier de votre projet :
electron App/main.js
  1. Dans la fenêtre principale de l’application, vous devez voir apparaître le contenu de votre fichier index.html, ainsi que le bouton Se connecter.

Tester la connexion et la déconnexion

Une fois le fichier index.html chargé, sélectionnez Se connecter. Vous êtes invité à vous connecter avec la plateforme d’identité Microsoft :

sign-in prompt

Si vous consentez à accorder les autorisations demandées, l’application web affiche votre nom d’utilisateur, ce qui signifie que la connexion a réussi :

successful sign-in

Tester l’appel à l’API web

Une fois connecté, sélectionnez See Profile pour afficher les informations de profil utilisateur retournées dans la réponse à l’appel à l’API Microsoft Graph. Après le consentement, vous verrez les informations de profil retournées dans la réponse :

profile information from Microsoft Graph

Fonctionnement de l’application

Quand un utilisateur sélectionne le bouton Sign In pour la première fois, la méthode acquireTokenInteractive du nœud MSAL. Cette méthode redirige l’utilisateur vers la connexion avec le point de terminaison Plateforme d’identités Microsoft et valide les informations d’identification de l’utilisateur, obtient un code d’autorisation, puis échange ce code contre un jeton d’ID, un jeton d’accès et un jeton d’actualisation. MSAL Node met également en cache ces jetons pour une utilisation ultérieure.

Le jeton d’ID contient des informations générales sur l’utilisateur, comme son nom d’affichage. Le jeton d’accès a une durée de vie limitée et expire au bout de 24 heures. Si vous envisagez d’utiliser ces jetons pour accéder à une ressource protégée, votre serveur back-end doit les valider afin de garantir leur émission pour un utilisateur autorisé de votre application.

L’application de bureau que vous avez créée dans ce tutoriel passe un appel REST à l’API Microsoft Graph au moyen d’un jeton d’accès utilisé comme jeton du porteur dans l’en-tête de requête (RFC 6750).

L’API Microsoft Graph nécessite l’étendue user.read pour lire le profil d’un utilisateur. Par défaut, cette étendue est automatiquement ajoutée à toutes les applications inscrites dans le Portail Azure. D’autres API pour Microsoft Graph et des API personnalisées pour votre serveur back-end peuvent nécessiter des étendues supplémentaires. Par exemple, l’API Microsoft Graph nécessite l’étendue Mail.Read afin de lister l’e-mail de l’utilisateur.

À mesure que vous ajoutez des étendues, vos utilisateurs peuvent être invités à accorder un consentement supplémentaire pour les étendues ajoutées.

Aide et support

Si vous avez besoin d’aide, si vous souhaitez signaler un problème ou si vous voulez en savoir plus sur vos options de support, consultez Aide et support pour les développeurs.

Étapes suivantes

Si vous souhaitez approfondir le développement d’une application de bureau Electron Node.js sur la plateforme d’identités Microsoft, consultez notre série de scénarios en plusieurs parties :