Compartir vía


Tutorial: Incorporación de un inicio de sesión en la aplicación web Node/Express.js mediante la plataforma de identidad de Microsoft

Se aplica a: Círculo verde con un símbolo de marca de verificación blanca que indica que el siguiente contenido se aplica a los inquilinos de la fuerza de trabajo. Círculo verde de inquilinos de recursos con un símbolo de marca de verificación blanca que indica que el siguiente contenido se aplica a los inquilinos externos. Inquilinos externos (más información)

En este tutorial, agregará lógica de inicio de sesión y cierre de sesión a la aplicación web Node/Express. Este código permite a los usuarios iniciar sesión en la aplicación orientada al cliente mediante un inquilino externo o empleador en un inquilino de recursos.

Este tutorial es la parte 2 de la serie de tutoriales de tres partes.

En este tutorial, harás lo siguiente:

  • Adición de lógica de inicio de sesión y cierre de sesión
  • Visualización de notificaciones de token de identificador
  • Ejecute la aplicación y pruebe la experiencia de inicio de sesión y cierre de sesión.

Prerrequisitos

Creación de un objeto de configuración de MSAL

En el editor de código, abra el archivo authConfig.js y agregue el siguiente código:

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

El objeto msalConfig contiene un conjunto de opciones de configuración que se usan para personalizar el comportamiento de los flujos de autenticación.

En el archivo authConfig.js, reemplace:

  • Enter_the_Application_Id_Here por el Id. de aplicación (cliente) de la aplicación que ha registrado antes.

  • Enter_the_Tenant_Subdomain_Here y reemplácelo por el subdominio de directorio externo (arrendatario). Por ejemplo, si el dominio principal del cliente es contoso.onmicrosoft.com, utilice contoso. Si no tiene el nombre del inquilino, aprenda a leer los detalles del inquilino. Este valor solo es necesario para el inquilino externo.

  • Enter_the_Client_Secret_Here con el valor secreto de aplicación que ha copiado antes.

  • Enter_the_Graph_Endpoint_Here con la instancia en la nube de Microsoft Graph API a la que llamará la aplicación. Utilice el valor https://graph.microsoft.com/ (incluya la barra diagonal al final)

Si usa el archivo .env para almacenar la información de configuración:

  1. En el editor de código, abra el archivo .env y agregue el código siguiente.

        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. Reemplace el marcador de posición:

    1. Enter_the_Application_Id_Here, Enter_the_Tenant_Subdomain_Here y Enter_the_Client_Secret_Here como se explicó anteriormente.
    2. Enter_the_Cloud_Instance_Id_Here con la instancia de nube de Azure en la que se registra la aplicación. Use https://login.microsoftonline.com/ como valor (incluya la barra diagonal final). Este valor solo es necesario para el inquilino de recursos.
    3. Enter_the_Tenant_ID_here con el id. de inquilino de recursos o el dominio principal, como aaaabbbb-0000-cccc-1111-dddd2222eeee o contoso.microsoft.com. Este valor solo es necesario para el inquilino de recursos.

Las variables , msalConfig, REDIRECT_URIy TENANT_SUBDOMAINGRAPH_ME_ENDPOINT se exportan POST_LOGOUT_REDIRECT_URIen el archivo authConfig.js para que sean accesibles en otros archivos.

URL de autoridad de tu aplicación

Las autoridades de aplicación para inquilinos externos y de recursos tienen un aspecto diferente. Debe crearlos como se muestra a continuación:

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

Uso del dominio de dirección URL personalizado (opcional)

Los dominios de dirección URL personalizados no se admiten en los inquilinos de recursos.

Agregar rutas de Express

Las rutas de Express proporcionan los puntos de conexión que nos permiten ejecutar operaciones como iniciar sesión, cerrar sesión y ver notificaciones de token de id.

Punto de entrada de la aplicación

En el editor de código, abra el archivo routes/index.js y agregue el siguiente código:

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 ruta / es el punto de entrada a la aplicación. Representa la vista views/index.hbs que creó anteriormente en Compilar componentes de la interfaz de usuario de la aplicación. isAuthenticated es una variable booleana que determina lo que ve en la vista.

Inicio y cierre de sesión

  1. En el editor de código, abra el archivo routes/auth.js y agregue el código siguiente:

    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. En el editor de código, abra el archivo controller/authController.js y agregue el código siguiente:

    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. En el editor de código, abra el archivo auth/AuthProvider.js y agregue el código siguiente:

    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;
    
    

    Las /signinrutas , /signout y /redirect se definen en el archivo routes/auth.js , pero se implementa su lógica en la clase auth/AuthProvider.js .

  • El método login maneja la ruta /signin:

    • Inicia el flujo de inicio de sesión desencadenando la primera etapa del flujo de código de autenticación.

    • Inicializa una instancia de aplicación cliente confidencial mediante el objeto de configuración MSAL, msalConfig, que creó anteriormente.

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

      El getMsalInstance método se define como:

          getMsalInstance(msalConfig) {
              return new msal.ConfidentialClientApplication(msalConfig);
          }
      
    • La primera etapa del flujo de código de autorización genera una dirección URL de solicitud de código de autorización y, a continuación, redirige a esa dirección URL para obtener el código de autorización. Esta primera etapa se implementa en el redirectToAuthCodeUrl método . Observe cómo usamos el método getAuthCodeUrl de las MSAL para generar la dirección URL del código de autorización:

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

      A continuación, se redirige a la propia dirección URL del código de autorización.

      //...
      res.redirect(authCodeUrlResponse);
      //...
      
  • El método handleRedirect maneja la ruta /redirect:

    • Estableciste esta dirección URL como URI de redirección para la aplicación web en el centro de administración de Microsoft Entra anteriormente en Inicio rápido: Inicio de sesión de usuarios en una aplicación web de ejemplo.

    • Este punto de conexión implementa la segunda etapa de usos del flujo de código de autenticación. Usa el código de autorización para solicitar un token de identificador mediante el método acquireTokenByCode de la MSAL.

      //...
      const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);
      //...
      
    • Después de recibir una respuesta, puede crear una sesión de Express y almacenar la información que desee en ella. Debe incluir isAuthenticated y establecerlo en true:

      //...        
      req.session.idToken = tokenResponse.idToken;
      req.session.account = tokenResponse.account;
      req.session.isAuthenticated = true;
      //...
      
  • El método logout maneja la ruta /signout:

    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);
        });
    }
    
    • Inicia la solicitud de cierre de sesión.

    • Cuando desee cerrar la sesión del usuario de la aplicación, no es suficiente con finalizar la sesión. Debe redirigir al usuario al logoutUri. De lo contrario, el usuario podría autenticarse de nuevo en las aplicaciones sin volver a introducir sus credenciales. Si el nombre del inquilino es contoso, el logoutUri es similar a https://contoso.ciamlogin.com/contoso.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000.

URI de cierre de sesión y punto de conexión de metadatos de autoridad para la aplicación

El URI de cierre de sesión de la aplicación, logoutUri y el punto de conexión de metadatos de autoridad, endpoint para inquilinos externos y de recursos tiene un aspecto diferente. Debe crearlos como se muestra a continuación:

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

Visualización de notificaciones de token de identificador

En el editor de código, abra el archivo routes/users.js y agregue el siguiente código:

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;

Si el usuario está autenticado, la ruta /id muestra las notificaciones de token de identificador mediante la vista views/id.hbs. Ha agregado esta vista anteriormente en Compilar componentes de la interfaz de usuario de la aplicación.

Para extraer una notificación de token de id. específica, como un nombre propio:

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

Finalización de la aplicación web

  1. En el editor de código, abra app.js archivo y agregue el código de app.js a él.

  2. En el editor de código, abra server.js archivo y agregue el código de server.js a él.

  3. En el editor de código, abra package.json archivo y, a continuación, actualice la scripts propiedad a:

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

Ejecución y prueba de la aplicación web Node/Express.js

Ahora puede probar su aplicación web con Node.js.

  1. Siga los pasos descritos en Creación de un usuario para crear un usuario de prueba en el inquilino de recursos. Si no tiene acceso al inquilino, pida al administrador de inquilinos que cree el usuario automáticamente.

  2. Para iniciar el servidor, ejecute los siguientes comandos desde el directorio del proyecto:

    npm start
    
  3. Abra el explorador y vaya a http://localhost:3000. Debería ver la página similar a la que aparece en la siguiente captura de pantalla:

    Captura de pantalla del inicio de sesión en una aplicación web de nodos.

  4. Seleccione Iniciar sesión para comenzar el proceso de inicio de sesión. La primera vez que inicie sesión, se le pedirá que proporcione su consentimiento para permitir que la aplicación inicie sesión y acceda a su perfil, como se muestra en la captura de pantalla siguiente:

    Captura de pantalla que muestra la pantalla de consentimiento del usuario

Una vez que haya iniciado sesión correctamente, se le redirigirá a la página principal de la aplicación.

Paso siguiente