Habilitación de la autenticación en su propia API web de Node.js mediante Azure Active Directory B2C

En este artículo, aprenderá a crear la aplicación web que llama a la API web. La API web debe protegerse mediante Azure Active Directory B2C (Azure AD B2C). Para autorizar el acceso a una API web, se atienden solicitudes que incluyen un token de acceso válido emitido por Azure AD B2C.

Requisitos previos

Paso 1: Creación de una API web protegida

Siga estos pasos para crear la API web de Node.js.

Paso 1.1: Creación del proyecto

Use Express para Node.js para crear una API web. Para crear una API web, haga lo siguiente:

  1. Cree una carpeta nueva denominada TodoList.
  2. En la carpeta TodoList, cree un archivo denominado index.js.
  3. En un shell de comandos, ejecute npm init -y. Este comando crea un archivo package.json predeterminado para el proyecto de Node.js.
  4. En el shell de comandos, ejecute npm install express. Este comando instala el marco Express.

Paso 1.2: Instalación de dependencias

Agregue la biblioteca de autenticación al proyecto de la API web. La biblioteca de autenticación analiza el encabezado de autenticación HTTP, valida el token y extrae las notificaciones. Para obtener más información, revise la documentación de la biblioteca.

Para agregar la biblioteca de autenticación, instale los paquetes mediante la ejecución del siguiente comando:

npm install passport
npm install passport-azure-ad
npm install morgan

El paquete morgan es middleware de registrador de solicitudes HTTP para Node.js.

Paso 1.3: Escritura del código del servidor de API web

En el archivo index.js, agregue el código siguiente:

const express = require('express');
const morgan = require('morgan');
const passport = require('passport');
const config = require('./config.json');
const todolist = require('./todolist');
const cors = require('cors');

//<ms_docref_import_azuread_lib>
const BearerStrategy = require('passport-azure-ad').BearerStrategy;
//</ms_docref_import_azuread_lib>

global.global_todos = [];

//<ms_docref_azureadb2c_options>
const options = {
    identityMetadata: `https://${config.credentials.tenantName}.b2clogin.com/${config.credentials.tenantName}.onmicrosoft.com/${config.policies.policyName}/${config.metadata.version}/${config.metadata.discovery}`,
    clientID: config.credentials.clientID,
    audience: config.credentials.clientID,
    policyName: config.policies.policyName,
    isB2C: config.settings.isB2C,
    validateIssuer: config.settings.validateIssuer,
    loggingLevel: config.settings.loggingLevel,
    passReqToCallback: config.settings.passReqToCallback
}

//</ms_docref_azureadb2c_options>

//<ms_docref_init_azuread_lib>
const bearerStrategy = new BearerStrategy(options, (token, done) => {
        // Send user info using the second argument
        done(null, { }, token);
    }
);
//</ms_docref_init_azuread_lib>
const app = express();

app.use(express.json()); 

//enable CORS (for testing only -remove in production/deployment)
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Authorization, Origin, X-Requested-With, Content-Type, Accept');
    next();
});

app.use(morgan('dev'));

app.use(passport.initialize());

passport.use(bearerStrategy);

// To do list endpoints
app.use('/api/todolist', todolist);

//<ms_docref_protected_api_endpoint>
// API endpoint, one must present a bearer accessToken to access this endpoint
app.get('/hello',
    passport.authenticate('oauth-bearer', {session: false}),
    (req, res) => {
        console.log('Validated claims: ', req.authInfo);
    
          
        // Service relies on the name claim.  
        res.status(200).json({'name': req.authInfo['name']});
    }
);
//</ms_docref_protected_api_endpoint>

//<ms_docref_anonymous_api_endpoint>
// API anonymous endpoint, returns a date to the caller.
app.get('/public', (req, res) => res.send( {'date': new Date() } ));
//</ms_docref_anonymous_api_endpoint>

const port = process.env.PORT || 5000;

app.listen(port, () => {
    console.log('Listening on port ' + port);
});

Tome nota de los siguientes fragmentos de código en el archivo index.js:

  • Importa la biblioteca de Azure AD de "passport".

    const BearerStrategy = require('passport-azure-ad').BearerStrategy;
    
  • Establece las opciones de Azure AD B2C.

    const options = {
        identityMetadata: `https://${config.credentials.tenantName}.b2clogin.com/${config.credentials.tenantName}.onmicrosoft.com/${config.policies.policyName}/${config.metadata.version}/${config.metadata.discovery}`,
        clientID: config.credentials.clientID,
        audience: config.credentials.clientID,
        policyName: config.policies.policyName,
        isB2C: config.settings.isB2C,
        validateIssuer: config.settings.validateIssuer,
        loggingLevel: config.settings.loggingLevel,
        passReqToCallback: config.settings.passReqToCallback
    }
    
  • Crea una instancia de la biblioteca de Azure AD de "passport" con las opciones de Azure AD B2C.

    const bearerStrategy = new BearerStrategy(options, (token, done) => {
            // Send user info using the second argument
            done(null, { }, token);
        }
    );
    
  • Punto de conexión de API protegido. Atiende a solicitudes que incluyen un token de acceso válido emitido por Azure AD B2C. Este punto de conexión devuelve el valor de la notificación name dentro del token de acceso.

    // API endpoint, one must present a bearer accessToken to access this endpoint
    app.get('/hello',
        passport.authenticate('oauth-bearer', {session: false}),
        (req, res) => {
            console.log('Validated claims: ', req.authInfo);
        
              
            // Service relies on the name claim.  
            res.status(200).json({'name': req.authInfo['name']});
        }
    );
    
  • Punto de conexión de API anónimo. La aplicación web puede llamarlo sin presentar un token de acceso. Úselo para depurar la API web con llamadas anónimas.

    // API anonymous endpoint, returns a date to the caller.
    app.get('/public', (req, res) => res.send( {'date': new Date() } ));
    

Paso 1.4: Configuración de la API web

Agregue configuraciones a un archivo de configuración. El archivo contiene información sobre el proveedor de identidades de Azure AD B2C. La aplicación de API web usa esta información para validar el token de acceso que la aplicación web pasa como token de portador.

  1. En la carpeta raíz del proyecto, cree un archivo config.json y, a continuación, agréguele el siguiente objeto JSON:

    {
        "credentials": {
            "tenantName": "fabrikamb2c",
            "clientID": "93733604-cc77-4a3c-a604-87084dd55348"
        },
        "policies": {
            "policyName": "B2C_1_susi"
        },
        "resource": {
            "scope": ["tasks.read"]
        },
        "metadata": {
            "authority": "login.microsoftonline.com",
            "discovery": ".well-known/openid-configuration",
            "version": "v2.0"
        },
        "settings": {
            "isB2C": true,
            "validateIssuer": true,
            "passReqToCallback": false,
            "loggingLevel": "info"
        }
    }
    
  2. En el archivo config.json, actualice las propiedades siguientes:

Sección Key Valor
credentials tenantName Primera parte del nombre de inquilino de Azure AD B2C (por ejemplo, fabrikamb2c).
credentials clientID Id. de aplicación de la API web. Para obtener información sobre cómo obtener el identificador de registro de la aplicación de API web, consulte Requisitos previos.
directivas policyName Flujos de usuario o directiva personalizada. Para obtener información sobre cómo obtener el flujo de usuario o la directiva, consulte Requisitos previos.
resource scope Ámbitos del registro de la aplicación de API web, como [tasks.read]. Para obtener información sobre cómo obtener el ámbito de la API web, consulte Requisitos previos.

Paso 2: Creación de la aplicación web de Node.

Siga estos pasos para crear la aplicación web de Node. Esta aplicación web autentica a un usuario para adquirir un token de acceso que se usa para llamar a la API web de Node que creó en el paso 1:

Paso 2.1: Creación del proyecto de nodo

Cree una carpeta para contener la aplicación de nodo, como call-protected-api.

  1. En el terminal, cambie el directorio a la carpeta de la aplicación de Node, como cd call-protected-api, y ejecute npm init -y. Este comando crea un archivo package.json predeterminado para el proyecto de Node.js.

  2. En el terminal, ejecute npm install express. Este comando instala el marco Express.

  3. Cree más carpetas y archivos para lograr esta estructura de proyecto:

    call-protected-api/
    ├── index.js
    └── package.json
    └── .env
    └── views/
        └── layouts/
            └── main.hbs
        └── signin.hbs
        └── api.hbs
    

    La carpeta views contiene archivos Handlebars para la interfaz de usuario de la aplicación web.

Paso 2.2: Instalación de las dependencias

En el terminal, ejecute los comandos siguientes para instalar los paquetes dotenv, express-handlebars, express-session y @azure/msal-node:

npm install dotenv
npm install express-handlebars
npm install express
npm install axios
npm install express-session
npm install @azure/msal-node

Paso 2.3: Compilación de componentes de la interfaz de usuario de la aplicación web

  1. En el archivo main.hbs, agregue el código siguiente:

    <!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">
        <title>Azure AD B2C | Enable authenticate on web API using MSAL for B2C</title>
    
        <!-- adding Bootstrap 4 for UI components  -->
        <!-- CSS only -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
        <link rel="SHORTCUT ICON" href="https://c.s-microsoft.com/favicon.ico?v2" type="image/x-icon">
      </head>
      <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
          <a class="navbar-brand" href="/">Microsoft Identity Platform</a>
            {{#if showSignInButton}}
                <div class="ml-auto">
                    <a type="button" id="SignIn" class="btn btn-success" href="/signin" aria-haspopup="true" aria-expanded="false">
                        Sign in to call PROTECTED API
                    </a>
                    <a type="button" id="SignIn" class="btn btn-warning" href="/api" aria-haspopup="true" aria-expanded="false">
                        Or call the ANONYMOUS API 
                     </a>
                </div>
            {{else}}
                    <p class="navbar-brand d-flex ms-auto">Hi {{givenName}}</p>
                    <a class="navbar-brand d-flex ms-auto" href="/signout">Sign out</a>
            {{/if}}
        </nav>
        <br>
        <h5 class="card-header text-center">MSAL Node Confidential Client application with Auth Code Flow</h5>
        <br>
        <div class="row" style="margin:auto" >
          {{{body}}}
        </div>
        <br>
        <br>
      </body>
    </html>
    

    El archivo main.hbs está en la carpeta layout y debe contener todo el código HTML que se necesita en la aplicación. Implementa la interfaz de usuario compilada con el marco Bootstrap 5 CSS. Cualquier interfaz de usuario que cambie de página a página, como signin.hbs, se coloca en el marcador de posición que se muestra como {{{body}}}.

  2. En el archivo signin.hbs, agregue el código siguiente:

    <div class="col-md-3" style="margin:auto">
      <div class="card text-center">
        <div class="card-body">
          {{#if showSignInButton}}
    
          {{else}}
               <h5 class="card-title">You have signed in</h5>
              <a type="button" id="Call-api" class="btn btn-success" href="/api" aria-haspopup="true" aria-expanded="false">
                  Call the PROTECTED API
              </a>
          {{/if}}
        </div>
        </div>
      </div>
    </div>
    
  3. En el archivo api.hbs, agregue el código siguiente:

    <div class="col-md-3" style="margin:auto">
      <div class="card text-center bg-{{bg_color}}">
        <div class="card-body">
    
              <h5 class="card-title">{{data}}</h5>
    
        </div>
      </div>
    </div>
    

    En esta página se muestra la respuesta de la API. El atributo de clase bg-{{bg_color}} de la tarjeta de Bootstrap permite que la interfaz de usuario muestre un color de fondo diferente para los distintos puntos de conexión de API.

Paso 2.4: Completado del código del servidor de la aplicación web

  1. En el archivo .env, agregue el código siguiente, que incluye el puerto HTTP del servidor, los detalles del registro de la aplicación y los detalles de directivas y flujos de usuario de inicio de sesión y registro:

    SERVER_PORT=3000
    #web apps client ID
    APP_CLIENT_ID=<You app client ID here>
    #session secret
    SESSION_SECRET=sessionSecretHere
    #web app client secret
    APP_CLIENT_SECRET=<Your app client secret here>
    #tenant name
    TENANT_NAME=<your-tenant-name>
    #B2C sign up and sign in user flow/policy name and authority
    SIGN_UP_SIGN_IN_POLICY_AUTHORITY=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name>
    AUTHORITY_DOMAIN=https://<your-tenant-name>.b2clogin.com
    #client redorect url
    APP_REDIRECT_URI=http://localhost:3000/redirect
    LOGOUT_ENDPOINT=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name>/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000
    

    Modifique los valores de los archivos .env como se explica en Configuración de la aplicación web de ejemplo.

  2. En el archivo index.js, agregue el código siguiente:

    /*
     * Copyright (c) Microsoft Corporation. All rights reserved.
     * Licensed under the MIT License.
     */
    require('dotenv').config();
    const express = require('express');
    const session = require('express-session');
    const {engine}  = require('express-handlebars');
    const msal = require('@azure/msal-node');
    //Use axios to make http calls 
    const axios = require('axios');
    
    //<ms_docref_configure_msal>
    /**
     * Confidential Client Application Configuration
     */
     const confidentialClientConfig = {
        auth: {
            clientId: process.env.APP_CLIENT_ID, 
            authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, 
            clientSecret: process.env.APP_CLIENT_SECRET,
            knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array
            redirectUri: process.env.APP_REDIRECT_URI,
            validateAuthority: false
        },
        system: {
            loggerOptions: {
                loggerCallback(loglevel, message, containsPii) {
                    console.log(message);
                },
                piiLoggingEnabled: false,
                logLevel: msal.LogLevel.Verbose,
            }
        }
    };
    
    // Initialize MSAL Node
    const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
    //</ms_docref_configure_msal>
    // Current web API coordinates were pre-registered in a B2C tenant.
    
    //<ms_docref_api_config>
    const apiConfig = {
        webApiScopes: [`https://${process.env.TENANT_NAME}.onmicrosoft.com/tasks-api/tasks.read`],
        anonymousUri: 'http://localhost:5000/public',
        protectedUri: 'http://localhost:5000/hello'
    };
    //</ms_docref_api_config>
    
    /**
     * The MSAL.js library allows you to pass your custom state as state parameter in the Request object
     * By default, MSAL.js passes a randomly generated unique state parameter value in the authentication requests.
     * 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.
     * For more information, visit: https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request
     */
    const APP_STATES = {
        LOGIN: 'login',
        CALL_API:'call_api'   
    }
    
    
    /** 
     * Request Configuration
     * We manipulate these two request objects below 
     * to acquire a token with the appropriate claims.
     */
     const authCodeRequest = {
        redirectUri: confidentialClientConfig.auth.redirectUri,
    };
    
    const tokenRequest = {
        redirectUri: confidentialClientConfig.auth.redirectUri,
    };
    
    
    /**
     * Using express-session middleware. Be sure to familiarize yourself with available options
     * and set them as desired. Visit: https://www.npmjs.com/package/express-session
     */
     const sessionConfig = {
        secret: process.env.SESSION_SECRET,
        resave: false,
        saveUninitialized: false,
        cookie: {
            secure: false, // set this to true on production
        }
    }
    //Create an express instance
    const app = express();
    
    //Set handlebars as your view engine
    app.engine('.hbs', engine({extname: '.hbs'}));
    app.set('view engine', '.hbs');
    app.set("views", "./views");
    
    app.use(session(sessionConfig));
    
    /**
     * This method is used to generate an auth code request
     * @param {string} authority: the authority to request the auth code from 
     * @param {array} scopes: scopes to request the auth code for 
     * @param {string} state: state of the application, tag a request
     * @param {Object} res: express middleware response object
     */
    
     const getAuthCode = (authority, scopes, state, res) => {
        // prepare the request
        console.log("Fetching Authorization code")
        authCodeRequest.authority = authority;
        authCodeRequest.scopes = scopes;
        authCodeRequest.state = state;
    
        //Each time you fetch Authorization code, update the authority in the tokenRequest configuration
        tokenRequest.authority = authority;
    
        // request an authorization code to exchange for a token
        return confidentialClientApplication.getAuthCodeUrl(authCodeRequest)
            .then((response) => {
                console.log("\nAuthCodeURL: \n" + response);
                //redirect to the auth code URL/send code to 
                res.redirect(response);
            })
            .catch((error) => {
                res.status(500).send(error);
            });
    }
    
    app.get('/', (req, res) => {
        res.render('signin', { showSignInButton: true });
    });
    
    
    
    app.get('/signin',(req, res)=>{ 
            //Initiate a Auth Code Flow >> for sign in
            //Pass the api scopes as well so that you received both the IdToken and accessToken
            getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,apiConfig.webApiScopes, APP_STATES.LOGIN, res);
    });
    
    
    app.get('/redirect',(req, res)=>{    
        
        if (req.query.state === APP_STATES.LOGIN) {
            // prepare the request for calling the web API
            tokenRequest.authority = process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY;
            tokenRequest.scopes = apiConfig.webApiScopes;
            tokenRequest.code = req.query.code;
            confidentialClientApplication.acquireTokenByCode(tokenRequest)
            .then((response) => {
                req.session.accessToken = response.accessToken;
                req.session.givenName = response.idTokenClaims.given_name;
                console.log('\nAccessToken:' + req.session.accessToken);
                res.render('signin', {showSignInButton: false, givenName: response.idTokenClaims.given_name});
            }).catch((error) => {
                console.log(error);
                res.status(500).send(error);
            });
        }else{
            res.status(500).send('We do not recognize this response!');
        }
    });
    
    //<ms_docref_api_express_route>
    app.get('/api', async (req, res) => {
        if(!req.session.accessToken){
            //User is not logged in and so they can only call the anonymous API
            try {
                const response = await axios.get(apiConfig.anonymousUri);
                console.log('API response' + response.data); 
                res.render('api',{data: JSON.stringify(response.data), showSignInButton: true, bg_color:'warning'});
            } catch (error) {
                console.error(error);
                res.status(500).send(error);
            }         
        }else{
            //Users have the accessToken because they signed in and the accessToken is still in the session
            console.log('\nAccessToken:' + req.session.accessToken);
            let accessToken = req.session.accessToken;
            const options = {
                headers: {
                    //accessToken used as bearer token to call a protected API
                    Authorization: `Bearer ${accessToken}`
                }
            };
    
            try {
                const response = await axios.get(apiConfig.protectedUri, options);
                console.log('API response' + response.data); 
                res.render('api',{data: JSON.stringify(response.data), showSignInButton: false, bg_color:'success', givenName: req.session.givenName});
            } catch (error) {
                console.error(error);
                res.status(500).send(error);
            }
        }     
    });
    
    //</ms_docref_api_express_route>
    
    /**
     * Sign out end point
    */
    app.get('/signout',async (req, res)=>{    
        logoutUri = process.env.LOGOUT_ENDPOINT;
        req.session.destroy(() => {
            res.redirect(logoutUri);
        });
    });
    app.listen(process.env.SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port !` + process.env.SERVER_PORT));
    

    El código del archivo index.js consta de variables globales y rutas rápidas.

    Variables globales:

    • confidentialClientConfig: el objeto de configuración de MSAL que se usa para crear el objeto de aplicación cliente confidencial.

      /**
       * Confidential Client Application Configuration
       */
       const confidentialClientConfig = {
          auth: {
              clientId: process.env.APP_CLIENT_ID, 
              authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, 
              clientSecret: process.env.APP_CLIENT_SECRET,
              knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array
              redirectUri: process.env.APP_REDIRECT_URI,
              validateAuthority: false
          },
          system: {
              loggerOptions: {
                  loggerCallback(loglevel, message, containsPii) {
                      console.log(message);
                  },
                  piiLoggingEnabled: false,
                  logLevel: msal.LogLevel.Verbose,
              }
          }
      };
      
      // Initialize MSAL Node
      const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
      
    • apiConfig: contiene la propiedad webApiScopes (su valor debe ser una matriz), que son los ámbitos configurados en la API web y concedidos a la aplicación web. También tiene URI en la API web a la que se va a llamar, es decir, anonymousUri y protectedUri.

      const apiConfig = {
          webApiScopes: [`https://${process.env.TENANT_NAME}.onmicrosoft.com/tasks-api/tasks.read`],
          anonymousUri: 'http://localhost:5000/public',
          protectedUri: 'http://localhost:5000/hello'
      };
      
    • APP_STATES: un valor incluido en la solicitud que también se devolverá en la respuesta del token. Se usa para diferenciar entre las respuestas recibidas de Azure AD B2C.

    • authCodeRequest: el objeto de configuración que se usa para recuperar el código de autorización.

    • tokenRequest: el objeto de configuración que se usa para adquirir un token mediante el código de autorización.

    • sessionConfig: el objeto de configuración de la sesión rápida.

    • getAuthCode: método que crea la dirección URL de la solicitud de autorización, lo que permite al usuario introducir las credenciales y el consentimiento en la aplicación. Usa el método getAuthCodeUrl, que se define en la clase ConfidentialClientApplication.

    Rutas rápidas:

    • /:
      • Es la entrada a la aplicación web y representa la página signin.
    • /signin:
      • Inicia la sesión del usuario.
      • Llama al método getAuthCode() y le pasa el elemento authority para la directiva o flujo de usuario de inicio de sesión y registro, APP_STATES.LOGIN y apiConfig.webApiScopes.
      • Esto hace que se desafíe al usuario final a que escriba sus inicios de sesión o, si el usuario no tiene una cuenta, a que se registre.
      • La respuesta final que resulta de este punto de conexión incluye un código de autorización de B2C que se devuelve al punto de conexión /redirect.
    • /redirect:
      • Se trata del punto de conexión establecido como URI de redireccionamiento para la aplicación web en Azure Portal.
      • Usa el parámetro de consulta state en la respuesta de Azure AD B2C, para diferenciar entre las solicitudes realizadas desde la aplicación web.
      • Si el estado de la aplicación es APP_STATES.LOGIN, el código de autorización adquirido se usa para recuperar un token a través del método acquireTokenByCode(). Cuando solicita un token a través del método acquireTokenByCode, usa los mismos ámbitos usados al adquirir el código de autorización. El token adquirido incluye accessToken, idToken y idTokenClaims. Después de adquirir accessToken, lo coloca en una sesión para su uso posterior para llamar a la API web.
    • /api:
      • Llama a la API web.
      • Si no está accessToken en la sesión, llame al punto de conexión de API anónimo (http://localhost:5000/public); de lo contrario, llame al punto de conexión de API protegido (http://localhost:5000/hello).
    • /signout:
      • Cierra la sesión del usuario.
      • Borra la sesión de la aplicación web y realiza una llamada HTTP al punto de conexión de cierre de sesión de Azure AD B2C.

Paso 3: Ejecución de la aplicación web y la API

Siga los pasos descritos en Ejecución de la aplicación web y la API para probar la aplicación web y la API web.

Pasos siguientes