Kurz: Přihlášení uživatelů a volání rozhraní Microsoft Graph API v desktopové aplikaci Elektron
V tomto kurzu vytvoříte desktopovou aplikaci Elektron, která přihlašuje uživatele a volá Microsoft Graph pomocí toku autorizačního kódu s PKCE. Desktopová aplikace, kterou sestavíte, používá knihovnu MSAL (Microsoft Authentication Library) pro Node.js.
Postupujte podle kroků v tomto kurzu:
- Registrace aplikace na webu Azure Portal
- Vytvoření projektu desktopové aplikace Elektron
- Přidání logiky ověřování do aplikace
- Přidání metody pro volání webového rozhraní API
- Přidání podrobností o registraci aplikace
- Otestování aplikace
Požadavky
- Node.js
- Elektron
- Visual Studio Code nebo jiný editor kódu
Registrace aplikace
Nejprve proveďte kroky v části Registrace aplikace na platformě Microsoft Identity Platform a zaregistrujte aplikaci.
Pro registraci aplikace použijte následující nastavení:
- Název:
ElectronDesktopApp
(navrhované) - Podporované typy účtů: Účty pouze v adresáři organizace (jeden tenant)
- Typ platformy: Mobilní a desktopové aplikace
- Identifikátor URI přesměrování:
http://localhost
Vytvoření projektu
Vytvořte složku pro hostování aplikace, například ElectronDesktopApp.
Nejprve přejděte do adresáře projektu v terminálu a spusťte následující
npm
příkazy: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
Pak vytvořte složku s názvem Aplikace. V této složce vytvořte soubor s názvem index.html , který bude sloužit jako uživatelské rozhraní. Přidejte do ní následující kód:
<!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>
Dále vytvořte soubor s názvem main.js a přidejte následující kód:
/* * 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); });
Ve výše uvedeném fragmentu kódu inicializujeme objekt hlavního okna Elektron a vytvoříme některé obslužné rutiny událostí pro interakce s oknem Elektron. Importujeme také parametry konfigurace, vytvoříme instanci třídy authProvider pro zpracování přihlášení, odhlášení a získání tokenu a zavoláme rozhraní Microsoft Graph API.
Ve stejné složce (App) vytvořte další soubor s názvem renderer.js a přidejte následující kód:
// 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 rendereru jsou vystaveny skriptem předběžného načtení nalezeného v souboru preload.js , aby vykreslovací modul získal přístup k Node API
zabezpečenému a řízenému způsobu.
Pak vytvořte nový soubor preload.js a přidejte následující kód:
// 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)); }, });
Tento skript předběžného načtení zpřístupňuje rozhraní API rendereru, které poskytuje procesům rendereru řízený přístup k některým Node APIs
použitím kanálů IPC nakonfigurovaných pro komunikaci mezi hlavními a vykreslovacími procesy.
Nakonec vytvořte soubor s názvem constants.js, který uloží konstanty řetězců pro popis událostí aplikace:
/* * 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, }
Teď máte jednoduché grafické uživatelské rozhraní a interakce pro aplikaci Elektron. Po dokončení zbývající části kurzu by struktura souborů a složek projektu měla vypadat nějak takto:
ElectronDesktopApp/
├── App
│ ├── AuthProvider.js
│ ├── constants.js
│ ├── graph.js
│ ├── index.html
| ├── main.js
| ├── preload.js
| ├── renderer.js
│ ├── authConfig.js
├── package.json
Přidání logiky ověřování do aplikace
Ve složce Aplikace vytvořte soubor s názvem AuthProvider.js. Soubor AuthProvider.js bude obsahovat třídu zprostředkovatele ověřování, která bude zpracovávat přihlašovací údaje, odhlášení, získání tokenu, výběr účtu a související úlohy ověřování pomocí uzlu MSAL. Přidejte do ní následující kód:
/*
* 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;
Ve výše uvedeném fragmentu kódu jsme nejprve inicializovali uzel PublicClientApplication
MSAL předáním objektu konfigurace (msalConfig
). Pak jsme odhalili login
a getToken
metody, logout
které se mají volat hlavním modulem (main.js). V login
a getToken
, získáme ID a přístupové tokeny pomocí veřejného rozhraní API MSAL Node acquireTokenInteractive
.
Přidání sady Microsoft Graph SDK
Vytvořte soubor s názvem graph.js. Soubor graph.js bude obsahovat instanci klienta sady Microsoft Graph SDK pro usnadnění přístupu k datům v rozhraní Microsoft Graph API pomocí přístupového tokenu získaného uzlem 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;
Přidání podrobností o registraci aplikace
Vytvořte soubor prostředí pro uložení podrobností o registraci aplikace, které se použijí při získávání tokenů. Uděláte to tak, že vytvoříte soubor s názvem authConfig.js uvnitř kořenové složky ukázky (ElectronDesktopApp) a přidáte následující kód:
/*
* 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,
};
Vyplňte tyto podrobnosti hodnotami, které získáte z portálu pro registraci aplikací Azure:
Enter_the_Tenant_Id_here
by měla být jedna z následujících možností:- Pokud vaše aplikace podporuje účty v tomto organizačním adresáři, nahraďte tuto hodnotu ID tenanta nebo názvem tenanta. Například
contoso.microsoft.com
. - Pokud vaše aplikace podporuje účty v libovolném organizačním adresáři, nahraďte tuto hodnotu
organizations
hodnotou . - Pokud vaše aplikace podporuje účty v libovolném organizačním adresáři a osobních účtech Microsoft, nahraďte tuto hodnotu
common
hodnotou . - Chcete-li omezit podporu pouze na osobní účty Microsoft, nahraďte tuto hodnotu
consumers
hodnotou .
- Pokud vaše aplikace podporuje účty v tomto organizačním adresáři, nahraďte tuto hodnotu ID tenanta nebo názvem tenanta. Například
Enter_the_Application_Id_Here
: ID aplikace (klienta) aplikace, kterou jste zaregistrovali.Enter_the_Cloud_Instance_Id_Here
: Cloudová instance Azure, ve které je vaše aplikace zaregistrovaná.- V případě hlavního (nebo globálního) cloudu Azure zadejte
https://login.microsoftonline.com/
. - Pro národní cloudy (například Čína) najdete odpovídající hodnoty v národních cloudech.
- V případě hlavního (nebo globálního) cloudu Azure zadejte
Enter_the_Graph_Endpoint_Here
je instance rozhraní Microsoft Graph API, se kterým by aplikace měla komunikovat.- V případě globálního koncového bodu rozhraní Microsoft Graph API nahraďte obě instance tohoto řetězce řetězcem
https://graph.microsoft.com/
. - Koncové body v národních cloudových nasazeních najdete v dokumentaci k Microsoft Graphu v národních cloudových nasazeních .
- V případě globálního koncového bodu rozhraní Microsoft Graph API nahraďte obě instance tohoto řetězce řetězcem
Otestování aplikace
Dokončili jste vytváření aplikace a jste připraveni spustit desktopovou aplikaci Electron a otestovat její funkčnost.
- Spusťte aplikaci spuštěním následujícího příkazu v kořenové složce projektu:
electron App/main.js
- V hlavním okně aplikace byste měli vidět obsah souboru index.html a tlačítko Přihlásit se.
Otestování přihlášení a odhlášení
Po načtení souboru index.html vyberte Přihlásit se. Zobrazí se výzva k přihlášení pomocí platformy Microsoft Identity Platform:
Pokud souhlasíte s požadovanými oprávněními, webové aplikace zobrazí vaše uživatelské jméno a podepisují úspěšné přihlášení:
Testování volání webového rozhraní API
Po přihlášení vyberte Zobrazit profil a zobrazte informace o profilu uživatele vrácené v odpovědi z volání rozhraní Microsoft Graph API. Po vyjádření souhlasu zobrazíte informace o profilu vrácené v odpovědi:
Jak aplikace funguje
Když uživatel poprvé vybere tlačítko Přihlásit se, acquireTokenInteractive
metoda uzlu MSAL. Tato metoda přesměruje uživatele na přihlášení pomocí koncového bodu Microsoft Identity Platform a ověří přihlašovací údaje uživatele, získá autorizační kód a pak tento kód pro token ID, přístupový token a obnovovací token. Uzel MSAL tyto tokeny také ukládá do mezipaměti pro budoucí použití.
Token ID obsahuje základní informace o uživateli, například zobrazované jméno. Přístupový token má omezenou životnost a vyprší po 24 hodinách. Pokud plánujete používat tyto tokeny pro přístup k chráněnému prostředku, back-endový server ho musí ověřit, aby se zajistilo, že token byl vydán platnému uživateli pro vaši aplikaci.
Desktopová aplikace, kterou jste vytvořili v tomto kurzu, volá rozhraní MICROSOFT Graph API pomocí přístupového tokenu jako nosný token v hlavičce požadavku (RFC 6750).
Rozhraní Microsoft Graph API vyžaduje obor user.read ke čtení profilu uživatele. Ve výchozím nastavení se tento obor automaticky přidá do každé aplikace zaregistrované na webu Azure Portal. Další rozhraní API pro Microsoft Graph a vlastní rozhraní API pro váš back-endový server můžou vyžadovat další obory. Rozhraní Microsoft Graph API například vyžaduje obor Mail.Read , aby bylo možné zobrazit seznam e-mailů uživatele.
Při přidávání oborů se uživatelům může zobrazit výzva k poskytnutí dalšího souhlasu s přidanými obory.
Nápověda a podpora
Pokud potřebujete pomoc, chcete nahlásit problém nebo se chcete dozvědět o možnostech podpory, přečtěte si nápovědu a podporu pro vývojáře.
Další kroky
Pokud se chcete podrobněji podívat na vývoj desktopových aplikací Node.js a Elektron na platformě Microsoft Identity Platform, projděte si naši sérii scénářů s více částmi: