Självstudie: Logga in användare och anropa Microsoft Graph API i en Electron-skrivbordsapp
I den här självstudien skapar du ett Elektron-skrivbordsprogram som loggar in användare och anropar Microsoft Graph med hjälp av auktoriseringskodflödet med PKCE. Skrivbordsappen som du skapar använder Microsoft Authentication Library (MSAL) för Node.js.
Följ stegen i den här självstudien för att:
- Registrera programmet i Azure-portalen
- Skapa ett Electron Desktop-appprojekt
- Lägga till autentiseringslogik i din app
- Lägga till en metod för att anropa ett webb-API
- Lägga till appregistreringsinformation
- Testa appen
Förutsättningar
- Node.js
- Elektron
- Visual Studio Code eller någon annan kodredigerare
Registrera programmet
Slutför först stegen i Registrera ett program med Microsoft platforma za identitete för att registrera din app.
Använd följande inställningar för din appregistrering:
- Namn:
ElectronDesktopApp
(föreslås) - Kontotyper som stöds: Endast konton i min organisationskatalog (enskild klientorganisation)
- Plattformstyp: Mobil- och skrivbordsprogram
- Omdirigerings-URI:
http://localhost
Skapa projektet
Skapa en mapp som värd för ditt program, till exempel ElectronDesktopApp.
Ändra först till projektkatalogen i terminalen och kör sedan följande
npm
kommandon: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
Skapa sedan en mapp med namnet App. I den här mappen skapar du en fil med namnet index.html som ska fungera som användargränssnitt. Lägg till följande kod där:
<!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>
Skapa sedan en fil med namnet main.js och lägg till följande kod:
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const path = require("path"); const { app, ipcMain, BrowserWindow } = require("electron"); const AuthProvider = require("./AuthProvider"); const { IPC_MESSAGES } = require("./constants"); const { protectedResources, msalConfig } = require("./authConfig"); const getGraphClient = require("./graph"); let authProvider; let mainWindow; function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, "preload.js") }, }); authProvider = new AuthProvider(msalConfig); } app.on("ready", () => { createWindow(); mainWindow.loadFile(path.join(__dirname, "./index.html")); }); app.on("window-all-closed", () => { app.quit(); }); app.on('activate', () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); // Event handlers ipcMain.on(IPC_MESSAGES.LOGIN, async () => { const account = await authProvider.login(); await mainWindow.loadFile(path.join(__dirname, "./index.html")); mainWindow.webContents.send(IPC_MESSAGES.SHOW_WELCOME_MESSAGE, account); }); ipcMain.on(IPC_MESSAGES.LOGOUT, async () => { await authProvider.logout(); await mainWindow.loadFile(path.join(__dirname, "./index.html")); }); ipcMain.on(IPC_MESSAGES.GET_PROFILE, async () => { const tokenRequest = { scopes: protectedResources.graphMe.scopes }; const tokenResponse = await authProvider.getToken(tokenRequest); const account = authProvider.account; await mainWindow.loadFile(path.join(__dirname, "./index.html")); const graphResponse = await getGraphClient(tokenResponse.accessToken) .api(protectedResources.graphMe.endpoint).get(); mainWindow.webContents.send(IPC_MESSAGES.SHOW_WELCOME_MESSAGE, account); mainWindow.webContents.send(IPC_MESSAGES.SET_PROFILE, graphResponse); });
I kodfragmentet ovan initierar vi ett Electron-huvudfönsterobjekt och skapar några händelsehanterare för interaktioner med elektronfönstret. Vi importerar även konfigurationsparametrar, instansierar klassen authProvider för hantering av inloggning, utloggning och tokenförvärv och anropar Microsoft Graph API.
I samma mapp (app) skapar du en annan fil med namnet renderer.js och lägger till följande kod:
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License /** * The renderer API is exposed by the preload script found in the preload.ts * file in order to give the renderer access to the Node API in a secure and * controlled way */ const welcomeDiv = document.getElementById('WelcomeMessage'); const signInButton = document.getElementById('signIn'); const signOutButton = document.getElementById('signOut'); const seeProfileButton = document.getElementById('seeProfile'); const cardDiv = document.getElementById('cardDiv'); const profileDiv = document.getElementById('profileDiv'); window.renderer.showWelcomeMessage((event, account) => { if (!account) return; cardDiv.style.display = 'initial'; welcomeDiv.innerHTML = `Welcome ${account.name}`; signInButton.hidden = true; signOutButton.hidden = false; }); window.renderer.handleProfileData((event, graphResponse) => { if (!graphResponse) return; console.log(`Graph API responded at: ${new Date().toString()}`); setProfile(graphResponse); }); // UI event handlers signInButton.addEventListener('click', () => { window.renderer.sendLoginMessage(); }); signOutButton.addEventListener('click', () => { window.renderer.sendSignoutMessage(); }); seeProfileButton.addEventListener('click', () => { window.renderer.sendSeeProfileMessage(); }); const setProfile = (data) => { if (!data) return; profileDiv.innerHTML = ''; const title = document.createElement('p'); const email = document.createElement('p'); const phone = document.createElement('p'); const address = document.createElement('p'); title.innerHTML = '<strong>Title: </strong>' + data.jobTitle; email.innerHTML = '<strong>Mail: </strong>' + data.mail; phone.innerHTML = '<strong>Phone: </strong>' + data.businessPhones[0]; address.innerHTML = '<strong>Location: </strong>' + data.officeLocation; profileDiv.appendChild(title); profileDiv.appendChild(email); profileDiv.appendChild(phone); profileDiv.appendChild(address); }
Återgivningsmetoderna exponeras av förinläsningsskriptet som finns i preload.js-filen för att ge renderaren åtkomst till på Node API
ett säkert och kontrollerat sätt
Skapa sedan en ny fil preload.js och lägg till följande kod:
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License const { contextBridge, ipcRenderer } = require('electron'); /** * This preload script exposes a "renderer" API to give * the Renderer process controlled access to some Node APIs * by leveraging IPC channels that have been configured for * communication between the Main and Renderer processes. */ contextBridge.exposeInMainWorld('renderer', { sendLoginMessage: () => { ipcRenderer.send('LOGIN'); }, sendSignoutMessage: () => { ipcRenderer.send('LOGOUT'); }, sendSeeProfileMessage: () => { ipcRenderer.send('GET_PROFILE'); }, handleProfileData: (func) => { ipcRenderer.on('SET_PROFILE', (event, ...args) => func(event, ...args)); }, showWelcomeMessage: (func) => { ipcRenderer.on('SHOW_WELCOME_MESSAGE', (event, ...args) => func(event, ...args)); }, });
Det här förinläsningsskriptet exponerar ett renderar-API för att ge återgivningsprocessen kontrollerad åtkomst till vissa Node APIs
genom att tillämpa IPC-kanaler som har konfigurerats för kommunikation mellan huvud- och återgivningsprocesserna.
Skapa slutligen en fil med namnet constants.js som lagrar strängarnas konstanter för att beskriva programhändelserna:
/* * 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, }
Nu har du ett enkelt GUI och interaktioner för din Electron-app. När du har slutfört resten av självstudien bör projektets fil- och mappstruktur se ut ungefär så här:
ElectronDesktopApp/
├── App
│ ├── AuthProvider.js
│ ├── constants.js
│ ├── graph.js
│ ├── index.html
| ├── main.js
| ├── preload.js
| ├── renderer.js
│ ├── authConfig.js
├── package.json
Lägga till autentiseringslogik i din app
Skapa en fil med namnet AuthProvider.js i appmappen. Filen AuthProvider.js innehåller en autentiseringsproviderklass som hanterar inloggning, utloggning, tokenförvärv, kontoval och relaterade autentiseringsuppgifter med MSAL Node. Lägg till följande kod där:
/*
* 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;
I kodfragmentet ovan initierade vi först MSAL Node PublicClientApplication
genom att skicka ett konfigurationsobjekt (msalConfig
). Sedan exponerade login
vi och getToken
logout
metoder som anropades av huvudmodulen (main.js). I login
och getToken
hämtar vi ID och åtkomsttoken med msal node acquireTokenInteractive
public API.
Lägga till Microsoft Graph SDK
Skapa en fil med namnet graph.js. Den graph.js filen innehåller en instans av Microsoft Graph SDK-klienten för att underlätta åtkomsten till data i Microsoft Graph-API:et med hjälp av den åtkomsttoken som hämtas av 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;
Lägga till appregistreringsinformation
Skapa en miljöfil för att lagra den appregistreringsinformation som ska användas vid anskaffning av token. Det gör du genom att skapa en fil med namnet authConfig.js i rotmappen i exemplet (ElectronDesktopApp) och lägga till följande kod:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const { LogLevel } = require("@azure/msal-node");
/**
* Configuration object to be passed to MSAL instance on creation.
* For a full list of MSAL.js configuration parameters, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
*/
const AAD_ENDPOINT_HOST = "Enter_the_Cloud_Instance_Id_Here"; // include the trailing slash
const msalConfig = {
auth: {
clientId: "Enter_the_Application_Id_Here",
authority: `${AAD_ENDPOINT_HOST}Enter_the_Tenant_Info_Here`,
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: LogLevel.Verbose,
},
},
};
/**
* Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
*/
const GRAPH_ENDPOINT_HOST = "Enter_the_Graph_Endpoint_Here"; // include the trailing slash
const protectedResources = {
graphMe: {
endpoint: `${GRAPH_ENDPOINT_HOST}v1.0/me`,
scopes: ["User.Read"],
}
};
module.exports = {
msalConfig: msalConfig,
protectedResources: protectedResources,
};
Fyll i den här informationen med de värden som du får från Azure-appregistreringsportalen:
Enter_the_Tenant_Id_here
bör vara något av följande:- Om ditt program stöder konton i den här organisationskatalogen ersätter du det här värdet med klientorganisations-ID:t eller klientorganisationens namn. Exempel:
contoso.microsoft.com
- Om ditt program stöder konton i en organisationskatalog ersätter du det här värdet med
organizations
. - Om ditt program stöder konton i en organisationskatalog och personliga Microsoft-konton ersätter du det här värdet med
common
. - Om du bara vill begränsa stödet till personliga Microsoft-konton ersätter du det här värdet med
consumers
.
- Om ditt program stöder konton i den här organisationskatalogen ersätter du det här värdet med klientorganisations-ID:t eller klientorganisationens namn. Exempel:
Enter_the_Application_Id_Here
: Program-ID :t (klient) för det program som du registrerade.Enter_the_Cloud_Instance_Id_Here
: Azure-molninstansen där ditt program är registrerat.- För det huvudsakliga (eller globala) Azure-molnet anger du
https://login.microsoftonline.com/
. - För nationella moln (till exempel Kina) kan du hitta lämpliga värden i nationella moln.
- För det huvudsakliga (eller globala) Azure-molnet anger du
Enter_the_Graph_Endpoint_Here
är instansen av Microsoft Graph API som programmet ska kommunicera med.- För den globala Microsoft Graph API-slutpunkten ersätter du båda instanserna av den här strängen med
https://graph.microsoft.com/
. - Slutpunkter i nationella molndistributioner finns i Nationella molndistributioner i Microsoft Graph-dokumentationen.
- För den globala Microsoft Graph API-slutpunkten ersätter du båda instanserna av den här strängen med
Testa appen
Du har slutfört skapandet av programmet och är nu redo att starta Electron Desktop-appen och testa appens funktioner.
- Starta appen genom att köra följande kommando från roten i projektmappen:
electron App/main.js
- I programmets huvudfönster bör du se innehållet i din index.html-fil och knappen Logga in .
Testa inloggning och utloggning
När index.html filen har lästs in väljer du Logga in. Du uppmanas att logga in med Microsoft platforma za identitete:
Om du godkänner de begärda behörigheterna visar webbprogrammen ditt användarnamn, vilket betecknar en lyckad inloggning:
Testa webb-API-anrop
När du har loggat in väljer du Se profil för att visa användarprofilinformationen som returnerades i svaret från anropet till Microsoft Graph-API:et. Efter medgivande visar du profilinformationen som returneras i svaret:
Så här fungerar programmet
När en användare väljer knappen Logga in för första gången, acquireTokenInteractive
metoden för MSAL Node. Den här metoden omdirigerar användaren till inloggning med Microsoft platforma za identitete-slutpunkten och validerar användarens autentiseringsuppgifter, hämtar en auktoriseringskod och utbyter sedan koden för en ID-token, åtkomsttoken och uppdateringstoken. MSAL Node cachelagrar även dessa token för framtida användning.
ID-token innehåller grundläggande information om användaren, till exempel deras visningsnamn. Åtkomsttoken har en begränsad livslängd och upphör att gälla efter 24 timmar. Om du planerar att använda dessa token för åtkomst till en skyddad resurs måste serverdelsservern verifiera den för att garantera att token har utfärdats till en giltig användare för ditt program.
Skrivbordsappen som du har skapat i den här självstudien gör ett REST-anrop till Microsoft Graph API med hjälp av en åtkomsttoken som ägartoken i begärandehuvudet (RFC 6750).
Microsoft Graph API kräver user.read-omfånget för att läsa en användares profil. Som standard läggs det här omfånget automatiskt till i varje program som är registrerat i Azure-portalen. Andra API:er för Microsoft Graph och anpassade API:er för serverdelsservern kan kräva extra omfång. Till exempel kräver Microsoft Graph API-omfånget Mail.Read för att kunna visa användarens e-post.
När du lägger till omfång kan användarna uppmanas att ge ett annat medgivande för de tillagda omfången.
Hjälp och support
Om du behöver hjälp, vill rapportera ett problem eller vill lära dig mer om dina supportalternativ kan du läsa Hjälp och support för utvecklare.
Nästa steg
Om du vill fördjupa dig i Node.js och utveckling av Elektron-skrivbordsprogram på Microsoft platforma za identitete kan du läsa vår serie med flerdelade scenarion: