Tutorial: Anmelden von Benutzern und Aufrufen der Microsoft Graph-API in einer Electron-Desktop-App
In diesem Tutorial wird eine Electron-Desktop-Anwendung erstellt, die Benutzer anmeldet und Microsoft Graph über den Autorisierungscodeflow mit PKCE aufruft. Die von Ihnen erstellte Desktop-App verwendet die Microsoft-Authentifizierungsbibliothek (Microsoft Authentication Library, MSAL) für Node.js.
Führen Sie die Schritte in diesem Tutorial für folgende Aktionen aus:
- Registrieren der Anwendung im Azure-Portal
- Erstellen eines Projekts für eine Electron-Desktop-App
- Hinzufügen von Authentifizierungslogik zur App
- Hinzufügen einer Methode zum Anrufen einer Web-API
- Hinzufügen von App-Registrierungsdetails
- Testen der App
Voraussetzungen
- Node.js
- Electron
- Visual Studio Code oder ein anderer Code-Editor
Registrieren der Anwendung
Führen Sie zunächst die Schritte unter Schnellstart: Registrieren einer Anwendung bei Microsoft Identity Platform aus, um Ihre App zu registrieren.
Verwenden Sie die folgenden Einstellungen für Ihre App-Registrierung:
- Name:
ElectronDesktopApp
(Vorschlag) - Unterstützte Kontotypen: Nur Konten in meinem Organisationsverzeichnis (einzelner Mandant)
- Plattformtyp: Mobile Anwendungen und Desktopanwendungen
- Umleitungs-URI:
http://localhost
Erstellen des Projekts
Erstellen Sie einen Ordner zum Hosten Ihrer Anwendung, z. B. ElectronDesktopApp.
Wechseln Sie zunächst in Ihrem Terminal zu Ihrem Projektverzeichnis, und führen Sie dann die folgenden
npm
-Befehle aus: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
Erstellen Sie anschließend einen Ordner mit dem Namen App. Erstellen Sie in diesem Ordner eine Datei mit dem Namen index.html, die als Benutzeroberfläche dient. Fügen Sie dort den folgenden Code hinzu:
<!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>
Erstellen Sie als Nächstes eine Datei namens main.js, und fügen Sie den folgenden Code hinzu:
/* * 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); });
Im obigen Codeausschnitt initialisieren Sie ein Electron-Hauptfensterobjekt und erstellen einige Ereignishandler für Interaktionen mit dem Electron-Fenster. Außerdem importieren Sie Konfigurationsparameter, instanziieren die Klasse authProvider für die Behandlung von Anmeldung, Abmeldung und Tokenabruf und rufen die Microsoft Graph-API auf.
Im gleichen Ordner (App) erstellen Sie eine weitere Datei mit dem Namen renderer.js und fügen den folgenden Code hinzu:
// 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); }
Die Renderermethoden werden durch das Preload-Skript in der Datei preload.js verfügbar gemacht, um dem Renderer sicheren und kontrollierten Zugriff auf die Node API
zu ermöglichen.
Erstellen Sie dann eine neue Datei mit dem Namen preload.js, und fügen Sie den folgenden Code hinzu:
// 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)); }, });
Dieses im Voraus geladene Skript macht eine Renderer-API verfügbar, um dem Rendererprozess kontrollierten Zugriff auf einige Node APIs
zu gewähren, indem IPC-Kanäle angewandt werden, die für die Kommunikation zwischen dem Haupt- und dem Rendererprozess konfiguriert wurden.
Erstellen Sie abschließend eine Datei mit dem Namen constants.js, in der die Zeichenfolgenkonstanten zum Beschreiben der Anwendungsereignisse gespeichert werden:
/* * 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, }
Sie verfügen jetzt über eine einfache GUI und Interaktionen für Ihre Electron-App. Nachdem Sie den Rest des Tutorials abgeschlossen haben, sollte die Datei- und Ordnerstruktur Ihres Projekts in etwa wie folgt aussehen:
ElectronDesktopApp/
├── App
│ ├── AuthProvider.js
│ ├── constants.js
│ ├── graph.js
│ ├── index.html
| ├── main.js
| ├── preload.js
| ├── renderer.js
│ ├── authConfig.js
├── package.json
Hinzufügen von Authentifizierungslogik zur App
Erstellen Sie im Ordner App eine Datei namens AuthProvider.js. Die Datei AuthProvider.js enthält eine Authentifizierungsanbieterklasse, die Anmeldung, Abmeldung, Tokenabruf, Kontoauswahl und zugehörige Authentifizierungsaufgaben mithilfe von MSAL Node verarbeitet. Fügen Sie dort den folgenden Code hinzu:
/*
* 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;
Im obigen Codeausschnitt haben Sie durch Übergabe eines Konfigurationsobjekts (msalConfig
) zuerst das MSAL Node-Element PublicClientApplication
initialisiert. Anschließend haben Sie die Methoden login
, logout
und getToken
verfügbar gemacht, die vom Hauptmodul (main.js) aufgerufen werden sollen. Rufen Sie in login
und getToken
jeweils ID- und Zugriffstoken über die öffentliche MSAL Node-API acquireTokenInteractive
ab.
Hinzufügen des Microsoft Graph SDK
Erstellen Sie eine Datei namens graph.js. Die Datei graph.js enthält eine Instanz des Microsoft Graph SDK-Clients für den Zugriff auf Daten in der Microsoft Graph-API, bei dem das von MSAL Node abgerufene Zugriffstoken verwendet wird:
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;
Hinzufügen von App-Registrierungsdetails
Erstellen Sie eine Umgebungsdatei zum Speichern der App-Registrierungsdetails, die beim Abrufen von Token verwendet werden. Erstellen Sie hierzu im Stammordner des Beispiels (ElectronDesktopApp) eine Datei mit dem Namen authConfig.js, und fügen Sie den folgenden Code hinzu:
/*
* 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,
};
Füllen Sie diese Details mit den Werten aus, die Sie aus dem Azure-Portal für die App-Registrierung abrufen:
Enter_the_Tenant_Id_here
sollte einer der folgenden Werte sein:- Wenn Ihre Anwendung Nur Konten in diesem Organisationsverzeichnis unterstützt, ersetzen Sie diesen Wert durch die Mandanten-ID oder den Mandantennamen. Beispiel:
contoso.microsoft.com
. - Falls Ihre Anwendung Konten in einem beliebigen Organisationsverzeichnis unterstützt, ersetzen Sie diesen Wert durch
organizations
. - Unterstützt Ihre Anwendung Konten in allen Organisationsverzeichnissen und persönliche Microsoft-Konten, ersetzen Sie diesen Wert durch
common
. - Wenn Sie die Unterstützung ausschließlich auf persönliche Microsoft-Konten beschränken möchten, ersetzen Sie diesen Wert durch
consumers
.
- Wenn Ihre Anwendung Nur Konten in diesem Organisationsverzeichnis unterstützt, ersetzen Sie diesen Wert durch die Mandanten-ID oder den Mandantennamen. Beispiel:
Enter_the_Application_Id_Here
: Die Anwendungs-ID (Client) der von Ihnen registrierten Anwendung.Enter_the_Cloud_Instance_Id_Here
: Die Azure Cloud-Instanz, in der Ihre Anwendung registriert ist.- Geben Sie für die Azure-Hauptcloud (oder die globale Cloud)
https://login.microsoftonline.com/
ein. - Geeignete Werte für nationale Clouds (z. B. für China) finden Sie unter Nationale Clouds.
- Geben Sie für die Azure-Hauptcloud (oder die globale Cloud)
Enter_the_Graph_Endpoint_Here
ist die Instanz der Microsoft Graph-API, mit der die Anwendung kommunizieren soll.- Ersetzen Sie beide Vorkommen dieser Zeichenfolge für den globalen Microsoft Graph-API-Endpunkt durch
https://graph.microsoft.com/
. - Informationen zu Endpunkten in nationalen Cloudbereitstellungen finden Sie in der Microsoft Graph Dokumentation unter Bereitstellungen nationaler Clouds.
- Ersetzen Sie beide Vorkommen dieser Zeichenfolge für den globalen Microsoft Graph-API-Endpunkt durch
Testen der App
Sie haben die Erstellung der Anwendung abgeschlossen und sind nun bereit, die Electron-Desktop-App zu starten und die Funktionalität der App zu testen.
- Starten Sie die App, indem Sie den folgenden Befehl im Stammverzeichnis Ihres Projektordners ausführen:
electron App/main.js
- Der Inhalt der Datei index.html und die Schaltfläche Anmelden sollten im Hauptfenster der Anwendung angezeigt werden.
Testen der An- und Abmeldung
Wählen Sie nach dem Laden der Datei index.html die Option Anmelden aus. Sie werden aufgefordert, sich mit Microsoft Identity Platform anzumelden:
Wenn Sie den angeforderten Berechtigungen zustimmen, zeigt die Webanwendung Ihren Benutzernamen an, was eine erfolgreiche Anmeldung signalisiert:
Testen des Web-API-Aufrufs
Nachdem Sie sich angemeldet haben, wählen Sie Profil anzeigen aus, um die Benutzerprofilinformationen anzuzeigen, die in der Antwort des Aufrufs der Microsoft Graph-API zurückgegeben werden. Nach der Einwilligung werden die in der Antwort zurückgegebenen Profilinformationen angezeigt:
Funktionsweise der Anwendung
Wenn Benutzer*innen die Schaltfläche Anmelden zum ersten Mal auswählen, wird die acquireTokenInteractive
-Methode von MSAL Node aufgerufen. Diese Methode leitet die Benutzer*innen zur Anmeldung mit dem Microsoft Identity Platform-Endpunkt um und überprüft die Anmeldeinformationen der Benutzer*innen, ruft einen Autorisierungscode ab und ersetzt diesen Code dann durch ein ID-Token, ein Zugriffstoken und ein Aktualisierungstoken. MSAL Node speichert diese Token auch für die zukünftige Verwendung zwischen.
Das ID-Token enthält grundlegende Informationen zum Benutzer, z. B. dessen Anzeigenamen. Das Zugriffstoken hat eine begrenzte Lebensdauer und läuft nach 24 Stunden ab. Wenn Sie planen, diese Token für den Zugriff auf eine geschützte Ressource zu nutzen, muss Ihr Back-End-Server diese überprüfen, um zu garantieren, dass das Token für einen gültigen Benutzer Ihrer Anwendung ausgestellt wurde.
Die von Ihnen in diesem Tutorial erstellte Desktop-App führt mithilfe eines Zugriffstokens als Bearertoken im Anforderungsheader (RFC 6750) einen REST-Aufruf für die Microsoft Graph-API aus.
Die Microsoft Graph-API benötigt den Bereich user.read, um das Benutzerprofil zu lesen. Dieser Bereich wird standardmäßig jeder Anwendung automatisch hinzugefügt, die im Azure-Portal registriert ist. Andere APIs für Microsoft Graph sowie benutzerdefinierte APIs für Ihren Back-End-Server erfordern möglicherweise zusätzliche Bereiche. Die Microsoft Graph-API benötigt beispielsweise den Bereich Mail.Read, um die E-Mail des Benutzers aufzuführen.
Wenn Sie Bereiche hinzufügen, werden Ihre Benutzer möglicherweise aufgefordert, eine weitere Zustimmung für die hinzugefügten Bereiche zu erteilen.
Hilfe und Support
Wenn Sie Hilfe benötigen, ein Problem melden möchten oder sich über Ihre Supportoptionen informieren möchten, finden Sie weitere Informationen unter Hilfe und Support für Entwickler.
Nächste Schritte
Wenn Sie sich ausführlicher mit der Entwicklung von Node.js- und Electron-Desktopanwendungen auf Microsoft Identity Platform beschäftigen möchten, können Sie sich die mehrteilige Szenarioreihe ansehen: