Condividi tramite


Esercitazione: Aggiungere la funzionalità di accesso a un'app web Node/Express.js usando Microsoft Identity Platform

si applica a: cerchio verde con un simbolo di spunta bianco. utenti aziendali cerchio verde con un simbolo di spunta bianco. utenti esterni (ulteriori informazioni)

In questa esercitazione si aggiungono le funzionalità di accesso e disconnessione alla tua applicazione web Node/Express. Questo codice consente di far accedere gli utenti alla tua app per i clienti, registrandoli tramite un tenant esterno o i dipendenti di un tenant della forza lavoro.

Questa esercitazione è la parte 2 della serie di esercitazioni in 3 parti.

In questa esercitazione si apprenderà come:

  • Aggiungere la logica di accesso e disconnessione
  • Visualizzare le attestazioni del token ID
  • Avviare l'app e testare l'esperienza di accesso e disconnessione.

Prerequisiti

Creare un oggetto di configurazione MSAL

Nell'editor di codice aprire authConfig.js file e quindi aggiungere il codice seguente:

require('dotenv').config();

const TENANT_SUBDOMAIN = process.env.TENANT_SUBDOMAIN || 'Enter_the_Tenant_Subdomain_Here';
const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/auth/redirect';
const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI || 'http://localhost:3000';
const GRAPH_ME_ENDPOINT = process.env.GRAPH_API_ENDPOINT + "v1.0/me" || 'Enter_the_Graph_Endpoint_Here';

/**
 * Configuration object to be passed to MSAL instance on creation.
 * For a full list of MSAL Node configuration parameters, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
 */
const msalConfig = {
    auth: {
        clientId: process.env.CLIENT_ID || 'Enter_the_Application_Id_Here', // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
        //For external tenant
        authority: process.env.AUTHORITY || `https://${TENANT_SUBDOMAIN}.ciamlogin.com/`, // replace "Enter_the_Tenant_Subdomain_Here" with your tenant name
        //For workforce tenant
        //authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID
        clientSecret: process.env.CLIENT_SECRET || 'Enter_the_Client_Secret_Here', // Client secret generated from the app registration in Azure portal
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: 'Info',
        },
    },
};

module.exports = {
    msalConfig,
    REDIRECT_URI,
    POST_LOGOUT_REDIRECT_URI,
    TENANT_SUBDOMAIN,
    GRAPH_ME_ENDPOINT
};

L'oggetto msalConfig contiene un set di opzioni di configurazione usate per personalizzare il comportamento dei flussi di autenticazione.

Nel file authConfig.js sostituire:

  • Enter_the_Application_Id_Here con l'ID applicazione (client) dell'app registrata in precedenza.

  • Enter_the_Tenant_Subdomain_Here e sostituirlo con il sottodominio Directory (tenant) esterno. Ad esempio, se il dominio primario del tenant è contoso.onmicrosoft.com, usare contoso. Se non hai il nome del tenant, scopri come leggere i dettagli del tenant. Questo valore è obbligatorio solo per il tenant esterno.

  • Enter_the_Client_Secret_Here con il codice segreto dell'app che hai copiato in precedenza.

  • Enter_the_Graph_Endpoint_Here con l'istanza cloud dell'API Microsoft Graph che verrà chiamata dall'app. Usare il valore https://graph.microsoft.com/ (includere la barra obliqua finale)

Se si utilizza il file .env per archiviare le informazioni di configurazione:

  1. Nell'editor di codice aprire file con estensione env, quindi aggiungere il codice seguente.

        CLIENT_ID=Enter_the_Application_Id_Here
        TENANT_SUBDOMAIN=Enter_the_Tenant_Subdomain_Here 
        CLOUD_INSTANCE="Enter_the_Cloud_Instance_Id_Here" # cloud instance string should end with a trailing slash
        TENANT_ID=Enter_the_Tenant_ID_here
        CLIENT_SECRET=Enter_the_Client_Secret_Here
        REDIRECT_URI=http://localhost:3000/auth/redirect
        POST_LOGOUT_REDIRECT_URI=http://localhost:3000
        GRAPH_API_ENDPOINT=Enter_the_Graph_Endpoint_Here # graph api endpoint string should end with a trailing slash
        EXPRESS_SESSION_SECRET=Enter_the_Express_Session_Secret_Here # express session secret, just any random text
    
  2. Sostituire il segnaposto:

    1. Enter_the_Application_Id_Here, Enter_the_Tenant_Subdomain_Here e Enter_the_Client_Secret_Here come illustrato in precedenza.
    2. Enter_the_Cloud_Instance_Id_Here con l'istanza cloud di Azure in cui è registrata l'applicazione. Usare https://login.microsoftonline.com/ come valore (includere la barra finale). Questo valore è obbligatorio solo per il tenant della forza lavoro.
    3. Enter_the_Tenant_ID_here con l'ID tenant della forza lavoro o il dominio primario, ad esempio aaaabbbb-0000-cccc-1111-dddd2222eeee o contoso.microsoft.com. Questo valore è obbligatorio solo per il tenant della forza lavoro.

Esportare msalConfig, REDIRECT_URI, TENANT_SUBDOMAIN, GRAPH_ME_ENDPOINT e POST_LOGOUT_REDIRECT_URI variabili nel file authConfig.js, per renderle accessibili in altri file.

URL di autorizzazione per la tua app

Le modalità di autorizzazione delle applicazioni per i tenant esterni e della forza lavoro sono diverse. Costruiscili come illustrato di seguito:

//Authority for workforce tenant
authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID

Usare un dominio URL personalizzato (facoltativo)

I domini URL personalizzati non sono supportati nei tenant della forza lavoro.

Aggiungere route rapide

Le rotte Express forniscono gli endpoint che consentono di eseguire operazioni come l'accesso, la disconnessione e la visualizzazione delle affermazioni del token ID.

Punto di ingresso dell'app

Nell'editor di codice aprire route/index.js file, quindi aggiungere il codice seguente:

const express = require('express');
const router = express.Router();

router.get('/', function (req, res, next) {
    res.render('index', {
        title: 'MSAL Node & Express Web App',
        isAuthenticated: req.session.isAuthenticated,
        username: req.session.account?.username !== '' ? req.session.account?.username : req.session.account?.name,
    });
});    
module.exports = router;

La route / è il punto di ingresso dell'applicazione. Esegue il rendering della vista views/index.hbs creata in precedenza in Creare componenti dell'interfaccia utente dell'app. isAuthenticated è una variabile booleana che determina ciò che viene visualizzato nella vista.

Accedere e disconnettersi

  1. Nell'editor di codice aprire route/auth.js file, quindi aggiungere il codice seguente:

    const express = require('express');
    const authController = require('../controller/authController');
    const router = express.Router();
    
    router.get('/signin', authController.signIn);
    router.get('/signout', authController.signOut);
    router.post('/redirect', authController.handleRedirect);
    
    module.exports = router;
    
  2. Nell'editor di codice aprire file controller/authController.js, quindi aggiungere il codice seguente:

    const authProvider = require('../auth/AuthProvider');
    
    exports.signIn = async (req, res, next) => {
        return authProvider.login(req, res, next);
    };
    
    exports.handleRedirect = async (req, res, next) => {
        return authProvider.handleRedirect(req, res, next);
    }
    
    exports.signOut = async (req, res, next) => {
        return authProvider.logout(req, res, next);
    };
    
    
  3. Nell'editor di codice aprire file di autenticazione/AuthProvider.js, quindi aggiungere il codice seguente:

    const msal = require('@azure/msal-node');
    const axios = require('axios');
    const { msalConfig, TENANT_SUBDOMAIN, REDIRECT_URI, POST_LOGOUT_REDIRECT_URI, GRAPH_ME_ENDPOINT} = require('../authConfig');
    
    class AuthProvider {
        config;
        cryptoProvider;
    
        constructor(config) {
            this.config = config;
            this.cryptoProvider = new msal.CryptoProvider();
        }
    
        getMsalInstance(msalConfig) {
            return new msal.ConfidentialClientApplication(msalConfig);
        }
    
        async login(req, res, next, options = {}) {
            // create a GUID for crsf
            req.session.csrfToken = this.cryptoProvider.createNewGuid();
    
            /**
             * The MSAL Node library allows you to pass your custom state as state parameter in the Request object.
             * The state parameter can also be used to encode information of the app's state before redirect.
             * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter.
             */
            const state = this.cryptoProvider.base64Encode(
                JSON.stringify({
                    csrfToken: req.session.csrfToken,
                    redirectTo: '/',
                })
            );
    
            const authCodeUrlRequestParams = {
                state: state,
    
                /**
                 * By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit:
                 * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
                 */
                scopes: [],
            };
    
            const authCodeRequestParams = {
                state: state,
    
                /**
                 * By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit:
                 * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
                 */
                scopes: [],
            };
    
            /**
             * If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will
             * make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making
             * metadata discovery calls, thereby improving performance of token acquisition process.
             */
            if (!this.config.msalConfig.auth.authorityMetadata) {
                const authorityMetadata = await this.getAuthorityMetadata();
                this.config.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
            }
    
            const msalInstance = this.getMsalInstance(this.config.msalConfig);
    
            // trigger the first leg of auth code flow
            return this.redirectToAuthCodeUrl(
                req,
                res,
                next,
                authCodeUrlRequestParams,
                authCodeRequestParams,
                msalInstance
            );
        }
    
        async handleRedirect(req, res, next) {
            const authCodeRequest = {
                ...req.session.authCodeRequest,
                code: req.body.code, // authZ code
                codeVerifier: req.session.pkceCodes.verifier, // PKCE Code Verifier
            };
    
            try {
                const msalInstance = this.getMsalInstance(this.config.msalConfig);
                msalInstance.getTokenCache().deserialize(req.session.tokenCache);
    
                const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);
    
                req.session.tokenCache = msalInstance.getTokenCache().serialize();
                req.session.idToken = tokenResponse.idToken;
                req.session.account = tokenResponse.account;
                req.session.isAuthenticated = true;
    
                const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state));
                res.redirect(state.redirectTo);
            } catch (error) {
                next(error);
            }
        }
    
        async logout(req, res, next) {
            /**
             * Construct a logout URI and redirect the user to end the
             * session with Microsoft Entra ID. For more information, visit:
             * https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
             */
            //For external tenant
            //const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;
    
            //For workforce tenant
            let logoutUri = `${this.config.msalConfig.auth.authority}/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;
            req.session.destroy(() => {
                res.redirect(logoutUri);
            });
        }
    
        /**
         * Prepares the auth code request parameters and initiates the first leg of auth code flow
         * @param req: Express request object
         * @param res: Express response object
         * @param next: Express next function
         * @param authCodeUrlRequestParams: parameters for requesting an auth code url
         * @param authCodeRequestParams: parameters for requesting tokens using auth code
         */
        async redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
            // Generate PKCE Codes before starting the authorization flow
            const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
    
            // Set generated PKCE codes and method as session vars
            req.session.pkceCodes = {
                challengeMethod: 'S256',
                verifier: verifier,
                challenge: challenge,
            };
    
            /**
             * By manipulating the request objects below before each request, we can obtain
             * auth artifacts with desired claims. For more information, visit:
             * https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationurlrequest
             * https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationcoderequest
             **/
    
            req.session.authCodeUrlRequest = {
                ...authCodeUrlRequestParams,
                redirectUri: this.config.redirectUri,
                responseMode: 'form_post', // recommended for confidential clients
                codeChallenge: req.session.pkceCodes.challenge,
                codeChallengeMethod: req.session.pkceCodes.challengeMethod,
            };
    
            req.session.authCodeRequest = {
                ...authCodeRequestParams,
                redirectUri: this.config.redirectUri,
                code: '',
            };
    
            try {
                const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
                res.redirect(authCodeUrlResponse);
            } catch (error) {
                next(error);
            }
        }
    
        /**
         * Retrieves oidc metadata from the openid endpoint
         * @returns
         */
        async getAuthorityMetadata() {
            // For external tenant
            //const endpoint = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/v2.0/.well-known/openid-configuration`;
    
            // For workforce tenant
            const endpoint = `${this.config.msalConfig.auth.authority}/v2.0/.well-known/openid-configuration`;
            try {
                const response = await axios.get(endpoint);
                return await response.data;
            } catch (error) {
                console.log(error);
            }
        }
    }
    
    const authProvider = new AuthProvider({
        msalConfig: msalConfig,
        redirectUri: REDIRECT_URI,
        postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI,
    });
    
    module.exports = authProvider;
    
    

    Le route /signin, /signout e /redirect vengono definite nel file routes/auth.js, ma si implementa la logica nella classe auth/AuthProvider.js.

  • Il metodo login gestisce /signin route:

    • Avvia il flusso di accesso attivando la prima fase del flusso del codice di autenticazione.

    • Inizializza un'istanza di applicazione client confidenziale utilizzando l'oggetto di configurazione MSAL msalConfigche hai creato in precedenza.

          const msalInstance = this.getMsalInstance(this.config.msalConfig);
      

      Il metodo getMsalInstance è definito come:

          getMsalInstance(msalConfig) {
              return new msal.ConfidentialClientApplication(msalConfig);
          }
      
    • La prima parte del flusso del codice di autenticazione genera un URL di richiesta del codice di autorizzazione, quindi reindirizza a tale URL per ottenere il codice di autorizzazione. Questa prima fase viene implementata nel metodo redirectToAuthCodeUrl. Si noti come si usa msals metodo getAuthCodeUrl per generare l'URL del codice di autorizzazione:

      //...
      const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
      //...
      

      Viene quindi eseguito il reindirizzamento all'URL del codice di autorizzazione stesso.

      //...
      res.redirect(authCodeUrlResponse);
      //...
      
  • Il metodo handleRedirect gestisce /redirect route:

    • Hai impostato questo URL come URI di reindirizzamento per l'app Web nel centro di amministrazione di Microsoft Entra in Avvio rapido: Effettuare l'accesso degli utenti in un'app Web di esempio.

    • Questo endpoint implementa la seconda parte del flusso del codice di autenticazione usato. Usa il codice di autorizzazione per richiedere un token ID usando il metodo acquireTokenBy Code di MSAL.

      //...
      const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);
      //...
      
    • Dopo aver ricevuto una risposta, è possibile creare una sessione Express e archiviare le informazioni desiderate. È necessario includere isAuthenticated e impostarlo su true:

      //...        
      req.session.idToken = tokenResponse.idToken;
      req.session.account = tokenResponse.account;
      req.session.isAuthenticated = true;
      //...
      
  • Il metodo logout gestisce /signout route:

    async logout(req, res, next) {
        /**
         * Construct a logout URI and redirect the user to end the
            * session with Azure AD. For more information, visit:
            * https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
            */
        const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;
    
        req.session.destroy(() => {
            res.redirect(logoutUri);
        });
    }
    
    • Avvia la richiesta di disconnessione.

    • Quando si vuole disconnettere l'utente dall'applicazione, non è sufficiente per terminare la sessione dell'utente. È necessario reindirizzare l'utente all'indirizzo logoutUri. In caso contrario, l'utente potrebbe essere in grado di ripetere l'autenticazione alle applicazioni senza immettere nuovamente le credenziali. Se il nome del tenant è contoso, allora il logoutUri appare simile a https://contoso.ciamlogin.com/contoso.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000.

Endpoint dei metadati dell'autorità e dell'URI di disconnessione per l'app

L'URI di disconnessione dell'app, logoutUri e l'endpoint dei metadati dell'autorità, endpoint per i tenant esterni e della forza lavoro ha un aspetto diverso. Costruiscili come mostrato di seguito:

//Logout URI for workforce tenant
const logoutUri = `${this.config.msalConfig.auth.authority}/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;

//authority metadata endpoint for workforce tenant
const endpoint = `${this.config.msalConfig.auth.authority}/v2.0/.well-known/openid-configuration`;

Visualizzare le attestazioni del token ID

Nell'editor di codice aprire route/users.js file, quindi aggiungere il codice seguente:

const express = require('express');
const router = express.Router();

// custom middleware to check auth state
function isAuthenticated(req, res, next) {
    if (!req.session.isAuthenticated) {
        return res.redirect('/auth/signin'); // redirect to sign-in route
    }

    next();
};

router.get('/id',
    isAuthenticated, // check if user is authenticated
    async function (req, res, next) {
        res.render('id', { idTokenClaims: req.session.account.idTokenClaims });
    }
);        
module.exports = router;

Se l'utente è autenticato, la route /id visualizza le attestazioni del token ID usando la vista views/id.hbs. Questa visualizzazione è stata aggiunta in precedenza in Creare componenti dell'interfaccia utente dell'app.

Per estrarre un'attestazione di token ID specifica, ad esempio nome specificato:

const givenName = req.session.account.idTokenClaims.given_name

Finalizzare l'app Web

  1. Nell'editor di codice aprire app.js file, quindi aggiungere il codice da app.js.

  2. Nell'editor di codice aprire server.js file, quindi aggiungere il codice da server.js.

  3. Nell'editor di codice aprire package.json file, quindi aggiornare la proprietà scripts in:

    "scripts": {
    "start": "node server.js"
    }
    

Eseguire e testare l'app Web Node/Express.js

A questo punto, è possibile testare l'app Web del nodo.

  1. Usare la procedura descritta in Creare un nuovo utente per creare un utente di test nel tenant della forza lavoro. Se non hai accesso al tenant, chiedi all'amministratore del tenant di creare l'utente per te.

  2. Per avviare il server, eseguire i comandi seguenti dall'interno della directory del progetto:

    npm start
    
  3. Aprire il browser, quindi passare a http://localhost:3000. La pagina dovrebbe essere simile alla schermata seguente:

    Screenshot dell'accesso a un'applicazione web Node.

  4. Selezionare Accedi per avviare il processo di accesso. La prima volta che si accede, viene richiesto di fornire il consenso per consentire all'applicazione di accedere e accedere al profilo, come illustrato nello screenshot seguente:

    Screenshot che mostra la schermata di consenso dell'utente

Dopo aver eseguito l'accesso, si verrà reindirizzati alla home page dell'applicazione.

Passaggio successivo