Bagikan melalui


Tutorial: Memasukkan pengguna dan memanggil Microsoft Graph API di aplikasi desktop Electron

Dalam tutorial ini, Anda akan membuat aplikasi desktop Electron yang memasukkan pengguna dan memanggil Microsoft Graph dengan menggunakan aliran kode otorisasi dengan PKCE. Aplikasi desktop yang Anda buat menggunakan Microsoft Authentication Library (MSAL) untuk Node.js.

Ikuti langkah-langkah dalam tutorial ini untuk:

  • Mendaftarkan aplikasi di portal Microsoft Azure
  • Membuat proyek aplikasi desktop Electron
  • Menambahkan logika autentikasi ke aplikasi
  • Menambahkan metode untuk memanggil API web
  • Menambahkan detail pendaftaran aplikasi
  • Menguji aplikasi

Prasyarat

Mendaftarkan aplikasi

Pertama, selesaikan langkah-langkah dalam Mendaftarkan aplikasi dengan platform identitas Microsoft untuk mendaftarkan aplikasi Anda.

Gunakan pengaturan berikut untuk pendaftaran aplikasi Anda:

  • Nama: ElectronDesktopApp (disarankan)
  • Jenis akun yang didukung: Akun hanya di direktori organisasi saya (penyewa tunggal)
  • Jenis platform: Aplikasi seluler dan desktop
  • URI Pengalihan: http://localhost

Membuat proyek

Buat folder untuk menghosting aplikasi, misalnya ElectronDesktopApp.

  1. Pertama, ubah ke direktori proyek di terminal Anda, lalu jalankan perintah npm berikut:

    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. Kemudian, buat folder bernama App. Di dalam folder ini, buat file bernama index.html yang akan berfungsi sebagai UI. Tambahkan kode berikut di sana:

    <!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. Selanjutnya, buat file bernama main.js dan tambahkan kode berikut:

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

Dalam cuplikan kode di atas, kami menginisialisasi objek jendela utama Electron dan membuat beberapa penangan peristiwa untuk interaksi dengan jendela Electron. Kami juga mengimpor parameter konfigurasi, memberi contoh kelas authProvider untuk menangani aktivitas masuk, keluar, dan perolehan token, serta memanggil Microsoft Graph API.

  1. Di folder yang sama (App), buat file lain bernama renderer.js dan tambahkan kode berikut:

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

Metode merender diekspos oleh skrip pemuat yang ditemukan dalam file preload.js untuk memberikan akses merender ke Node API dengan cara yang aman dan terkontrol

  1. Setelah itu, buat file baru preload.js dan tambahkan kode berikut:

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

Skrip pramuat ini mengekspos API perender untuk memberikan proses perender akses terkontrol ke beberapa Node APIs dengan menerapkan saluran IPC yang telah dikonfigurasi untuk komunikasi antara proses utama dan perender.

  1. Terakhir, buat file bernama constants.js yang akan menyimpan konstanta string untuk menjelaskan peristiwa aplikasi:

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

Anda kini memiliki GUI sederhana dan interaksi untuk aplikasi Electron. Setelah menyelesaikan sisa tutorial, struktur file dan folder proyek Anda akan terlihat seperti berikut:

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

Menambahkan logika autentikasi ke aplikasi

Di folder App, buat file bernama AuthProvider.js. File AuthProvider.js akan menyertakan kelas penyedia layanan autentikasi yang akan menggunakan MSAL Node untuk menangani login, logout, akuisisi token, pemilihan akun, dan tugas otentikasi lainnya. Tambahkan kode berikut di sana:

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

Dalam cuplikan kode di atas, kami terlebih dahulu menginisialisasi MSAL Node PublicClientApplication dengan melewati objek konfigurasi (msalConfig). Kemudian, kami mengekspos metode login, logout, getToken yang akan digunakan oleh modul utama (main.js). Di login dan getToken, kami memperoleh ID dan token akses menggunakan API publik MSAL Node acquireTokenInteractive .

Menambahkan Microsoft Graph SDK

Buat file bernama graph.js. File graph.js akan berisi instans Klien Microsoft Graph SDK untuk memfasilitasi akses data pada Microsoft Graph API, menggunakan token akses yang diperoleh oleh 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;

Menambahkan detail pendaftaran aplikasi

Buat file lingkungan untuk menyimpan detail pendaftaran aplikasi yang akan digunakan saat memperoleh token. Untuk melakukannya, buat file bernama authConfig.js di dalam direktori akar sampel (ElectronDesktopApp), dan tambahkan kode berikut:

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

Isi detail berikut dengan nilai yang diperoleh dari portal pendaftaran aplikasi Azure:

  • Enter_the_Tenant_Id_here harus menjadi salah satu hal berikut:
    • Jika aplikasi Anda mendukung akun di direktori organisasi ini, ganti nilai ini dengan ID Penyewa atau nama Penyewa. Contohnya:contoso.microsoft.com
    • Jika aplikasi Anda mendukung akun di direktori organisasi apa pun, ganti nilai ini dengan organizations.
    • Jika aplikasi Anda mendukung akun di direktori organisasi dan akun Microsoft pribadi apa pun, ganti nilai ini dengan common.
    • Untuk membatasi dukungan hanya untuk akun Microsoft pribadi, ganti nilai ini dengan consumers.
  • Enter_the_Application_Id_Here: ID Aplikasi (klien) dari aplikasi yang Anda daftarkan.
  • Enter_the_Cloud_Instance_Id_Here: Instans cloud Azure tempat aplikasi Anda terdaftar.
    • Untuk cloud Azure utama (atau global),masukkan https://login.microsoftonline.com/.
    • Untuk cloud nasional (misalnya, Tiongkok), Anda dapat menemukan nilai yang sesuai di Cloud nasional.
  • Enter_the_Graph_Endpoint_Here adalah instans Microsoft Graph API yang harus dikomunikasikan dengan aplikasi.
    • Untuk titik akhir Microsoft Graph API global, ganti kedua instans string ini dengan https://graph.microsoft.com/.
    • Untuk titik akhir dalam penyebaran cloud nasional, lihat Penyebaran cloud nasional dalam dokumentasi Microsoft Graph.

Menguji aplikasi

Anda telah menyelesaikan pembuatan aplikasi dan sekarang siap meluncurkan aplikasi desktop Electron dan menguji fungsi aplikasi.

  1. Mulai aplikasi dengan menjalankan perintah berikut dari dalam akar folder proyek Anda:
electron App/main.js
  1. Di jendela utama aplikasi, Anda akan melihat isi file index.html dan tombol Masuk.

Menguji proses masuk dan keluar

Setelah file index.html dimuat, pilih Masuk. Anda akan diminta untuk masuk dengan platform identitas Microsoft:

sign-in prompt

Jika Anda menyetujui izin yang diminta, aplikasi web akan menampilkan nama pengguna Anda, yang menandakan bahwa proses masuk berhasil:

successful sign-in

Menguji panggilan API web

Setelah Anda masuk, pilih Lihat Profil untuk melihat informasi profil pengguna yang dikembalikan dalam respons dari panggilan ke Microsoft Graph API. Setelah persetujuan, Anda akan melihat informasi profil yang dikembalikan dalam respons:

profile information from Microsoft Graph

Cara kerja aplikasi

Saat pengguna memilih tombol Masuk untuk pertama kalinya acquireTokenInteractive , metode MSAL Node. Metode ini mengalihkan pengguna untuk masuk dengan titik akhir platform identitas Microsoft dan memvalidasi kredensial pengguna, mendapatkan kode otorisasi lalu menukar kode tersebut dengan token ID, token akses, dan token refresh. MSAL Node juga menyimpan token ini untuk digunakan di masa mendatang.

Token ID berisi informasi dasar tentang pengguna, seperti nama tampilannya. Token akses memiliki masa pakai terbatas dan berakhir setelah 24 jam. Jika Anda berencana menggunakan token ini untuk mengakses sumber daya yang dilindungi, server back-end Anda harus memvalidasinya untuk menjamin bahwa token dikeluarkan untuk pengguna yang valid untuk aplikasi Anda.

Aplikasi desktop yang telah Anda buat dalam tutorial ini melakukan panggilan REST ke Microsoft Graph API menggunakan token akses sebagai token pembawa di header permintaan (RFC 6750).

Microsoft Graph API memerlukan cakupan user.read untuk membaca profil pengguna. Secara default, cakupan ini otomatis ditambahkan di setiap aplikasi yang terdaftar di portal Microsoft Azure. API lain untuk Microsoft Graph dan kustom API untuk server back-end, Anda mungkin memerlukan cakupan tambahan. Misalnya, Microsoft Graph API memerlukan cakupan Mail.Read untuk mencantumkan email pengguna.

Saat Anda menambahkan lebih banyak cakupan, pengguna Anda mungkin diminta memberikan persetujuan tambahan untuk cakupan tambahan.

Bantuan dan dukungan

Jika Anda memerlukan bantuan, ingin melaporkan masalah, atau ingin mempelajari opsi dukungan, lihat Bantuan dan dukungan bagi pengembang.

Langkah berikutnya

Jika Anda ingin mendalami pengembangan aplikasi desktop Electron dan Node.js di platform identitas Microsoft, lihat rangkaian skenario multi-bagian kami: