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
Antes de comenzar a leer y completar los pasos descritos en Configuración de la autenticación en una API web Node.js de ejemplo mediante Azure AD B2C. A continuación, siga los pasos descritos en este artículo para reemplazar la aplicación web y la API web de ejemplo o su propia API web.
Visual Studio Code u otro editor de código
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:
- Cree una carpeta nueva denominada
TodoList
. - En la carpeta
TodoList
, cree un archivo denominadoindex.js
. - En un shell de comandos, ejecute
npm init -y
. Este comando crea un archivopackage.json
predeterminado para el proyecto de Node.js. - 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 passport de Microsoft Entra
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 passport de Microsoft Entra 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.
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" } }
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
.
En el terminal, cambie el directorio a la carpeta de la aplicación de Node, como
cd call-protected-api
, y ejecutenpm init -y
. Este comando crea un archivo package.json predeterminado para el proyecto de Node.js.En el terminal, ejecute
npm install express
. Este comando instala el marco Express.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
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 carpetalayout
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, comosignin.hbs
, se coloca en el marcador de posición que se muestra como{{{body}}}
.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>
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
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.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 propiedadwebApiScopes
(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
yprotectedUri
.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étodogetAuthCodeUrl
, que se define en la clase ConfidentialClientApplication.
Rutas rápidas:
/
:- Es la entrada a la aplicación web y representa la página
signin
.
- Es la entrada a la aplicación web y representa la página
/signin
:- Inicia la sesión del usuario.
- Llama al método
getAuthCode()
y le pasa el elementoauthority
para la directiva o flujo de usuario de inicio de sesión y registro,APP_STATES.LOGIN
yapiConfig.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étodoacquireTokenByCode()
. Cuando solicita un token a través del métodoacquireTokenByCode
, usa los mismos ámbitos usados al adquirir el código de autorización. El token adquirido incluyeaccessToken
,idToken
yidTokenClaims
. Después de adquiriraccessToken
, 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.