Tutorial: Inicio de sesión de usuarios y adquisición de un token para Microsoft Graph en una aplicación web de Node.js y Express
En este tutorial se compila una aplicación web que inicia la sesión de los usuarios y adquiere tokens de acceso para llamar a Microsoft Graph. La aplicación web que cree usa la biblioteca de autenticación de Microsoft (MSAL) para Node.
Siga los pasos de este tutorial para:
- Registrar la aplicación en Azure Portal
- Crear un proyecto de aplicación web Express
- Instalar los paquetes de la biblioteca de autenticación
- Agregar detalles del registro de la aplicación
- Agregar código para el inicio de sesión del usuario
- Prueba de la aplicación
Para obtener más información, consulte el código de ejemplo que muestra cómo usar el nodo MSAL para iniciar sesión, cerrar sesión y adquirir un token de acceso para un recurso protegido, como Microsoft Graph.
Prerrequisitos
- Node.js
- Visual Studio Code u otro editor de código
Registro de la aplicación
En primer lugar, complete los pasos descritos en Registro de una aplicación en la plataforma de identidad de Microsoft para registrar la aplicación.
Use la siguiente configuración para el registro de la aplicación:
- Nombre:
ExpressWebApp
(sugerido) - Tipos de cuenta admitidos: Solo las cuentas de este directorio organizativo
- Tipo de plataforma: Web
- URI de redirección:
http://localhost:3000/auth/redirect
- Secreto de cliente:
*********
(registre este valor para usarlo en un paso posterior, se muestra solo una vez)
Creación del proyecto
Use la herramienta Generador de aplicaciones Express para crear un esqueleto de aplicación.
- En primer lugar, instale el paquete express-generator:
npm install -g express-generator
- Luego, cree un esqueleto de aplicación de la siguiente manera:
express --view=hbs /ExpressWebApp && cd /ExpressWebApp
npm install
Ya tiene una aplicación web express sencilla. La estructura de archivos y carpetas del proyecto será similar a la siguiente estructura de carpetas:
ExpressWebApp/
├── bin/
| └── wwww
├── public/
| ├── images/
| ├── javascript/
| └── stylesheets/
| └── style.css
├── routes/
| ├── index.js
| └── users.js
├── views/
| ├── error.hbs
| ├── index.hbs
| └── layout.hbs
├── app.js
└── package.json
Instalación de la biblioteca de autenticación
Busque la raíz del directorio del proyecto en un terminal e instale el paquete MSAL Node mediante npm.
npm install --save @azure/msal-node
Instalación de otras dependencias
En el ejemplo de aplicación web de este tutorial se usa el paquete express-session para la administración de sesiones, el paquete dotenv para leer los parámetros del entorno durante el desarrollo y axios para realizar llamadas de red a Microsoft Graph API. Instálelos mediante npm:
npm install --save express-session dotenv axios
Agregar detalles del registro de la aplicación
- Cree un archivo .env.dev en la raíz de la carpeta del proyecto. Después agregue el siguiente código:
CLOUD_INSTANCE="Enter_the_Cloud_Instance_Id_Here" # cloud instance string should end with a trailing slash
TENANT_ID="Enter_the_Tenant_Info_Here"
CLIENT_ID="Enter_the_Application_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"
Rellene estos detalles con los valores que se obtienen del portal de registro de aplicaciones de Azure:
Enter_the_Cloud_Instance_Id_Here
: Instancia en la nube de Azure en la que se registra la aplicación.- En la nube principal de Azure (o global), escriba
https://login.microsoftonline.com/
(incluya la barra diagonal final). - En el caso de las nubes nacionales (por ejemplo, China), puede encontrar los valores adecuados en las nubes nacionales.
- En la nube principal de Azure (o global), escriba
Enter_the_Tenant_Info_here
debe ser uno de los siguientes parámetros:- Si la aplicación admite cuentas de este directorio organizativo, reemplace este valor por los valores de Id. de inquilino o Nombre de inquilino. Por ejemplo,
contoso.microsoft.com
. - Si la aplicación admite cuentas de cualquier directorio organizativo, reemplace este valor por
organizations
. - Si la aplicación admite cuentas de cualquier directorio organizativo y cuentas Microsoft personales, reemplace este valor por
common
. - Para restringir la compatibilidad a solo cuentas de Microsoft personales, reemplace este valor por
consumers
.
- Si la aplicación admite cuentas de este directorio organizativo, reemplace este valor por los valores de Id. de inquilino o Nombre de inquilino. Por ejemplo,
Enter_the_Application_Id_Here
: el identificador de la aplicación (cliente) de la aplicación que registró.Enter_the_Client_secret
: sustituya este valor por el secreto de cliente que creó anteriormente. Para generar una nueva clave, use Certificados y secretos en la configuración del registro de aplicaciones en Azure Portal.
Advertencia
Cualquier secreto de texto no cifrado que haya en el código fuente supone un aumento del riesgo de seguridad. En este artículo se usa un secreto de cliente de texto no cifrado exclusivamente en aras de una mayor simplicidad. Use las credenciales de certificado, en lugar de secretos de cliente, en las aplicaciones cliente confidenciales, especialmente en las que desee implementar en producción.
Enter_the_Graph_Endpoint_Here
: instancia de nube de Microsoft Graph API a la que llama la aplicación. Como servicio principal de Microsoft Graph API (global), escribahttps://graph.microsoft.com/
(incluya la barra diagonal final).Enter_the_Express_Session_Secret_Here
: secreto usado para firmar la cookie de sesión de express. Elija una cadena aleatoria de caracteres por la que reemplazar esta cadena, como el secreto de cliente.
- Luego, cree un archivo de nombre authConfig.js en la raíz del proyecto para leer estos parámetros. Una vez creado, agregue allí el código siguiente:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
require('dotenv').config({ path: '.env.dev' });
/**
* 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, // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID, // Full directory URL, in the form of https://login.microsoftonline.com/<tenant>
clientSecret: process.env.CLIENT_SECRET // Client secret generated from the app registration in Azure portal
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: 3,
}
}
}
const REDIRECT_URI = process.env.REDIRECT_URI;
const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI;
const GRAPH_ME_ENDPOINT = process.env.GRAPH_API_ENDPOINT + "v1.0/me";
module.exports = {
msalConfig,
REDIRECT_URI,
POST_LOGOUT_REDIRECT_URI,
GRAPH_ME_ENDPOINT
};
Adición de código para el inicio de sesión de los usuarios y la adquisición de tokens
- Cree una carpeta denominada auth y agregue un nuevo archivo denominado AuthProvider.js en ella. Esto contendrá la clase AuthProvider, que encapsula la lógica de autenticación necesaria mediante MSAL Node. Agréguele el siguiente código:
const msal = require('@azure/msal-node');
const axios = require('axios');
const { msalConfig } = require('../authConfig');
class AuthProvider {
msalConfig;
cryptoProvider;
constructor(msalConfig) {
this.msalConfig = msalConfig
this.cryptoProvider = new msal.CryptoProvider();
};
login(options = {}) {
return async (req, res, next) => {
/**
* 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({
successRedirect: options.successRedirect || '/',
})
);
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: options.scopes || [],
redirectUri: options.redirectUri,
};
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: options.scopes || [],
redirectUri: options.redirectUri,
};
/**
* 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. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/performance.md
*/
if (!this.msalConfig.auth.cloudDiscoveryMetadata || !this.msalConfig.auth.authorityMetadata) {
const [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([
this.getCloudDiscoveryMetadata(this.msalConfig.auth.authority),
this.getAuthorityMetadata(this.msalConfig.auth.authority)
]);
this.msalConfig.auth.cloudDiscoveryMetadata = JSON.stringify(cloudDiscoveryMetadata);
this.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
}
const msalInstance = this.getMsalInstance(this.msalConfig);
// trigger the first leg of auth code flow
return this.redirectToAuthCodeUrl(
authCodeUrlRequestParams,
authCodeRequestParams,
msalInstance
)(req, res, next);
};
}
acquireToken(options = {}) {
return async (req, res, next) => {
try {
const msalInstance = this.getMsalInstance(this.msalConfig);
/**
* If a token cache exists in the session, deserialize it and set it as the
* cache for the new MSAL CCA instance. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
*/
if (req.session.tokenCache) {
msalInstance.getTokenCache().deserialize(req.session.tokenCache);
}
const tokenResponse = await msalInstance.acquireTokenSilent({
account: req.session.account,
scopes: options.scopes || [],
});
/**
* On successful token acquisition, write the updated token
* cache back to the session. For more, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
*/
req.session.tokenCache = msalInstance.getTokenCache().serialize();
req.session.accessToken = tokenResponse.accessToken;
req.session.idToken = tokenResponse.idToken;
req.session.account = tokenResponse.account;
res.redirect(options.successRedirect);
} catch (error) {
if (error instanceof msal.InteractionRequiredAuthError) {
return this.login({
scopes: options.scopes || [],
redirectUri: options.redirectUri,
successRedirect: options.successRedirect || '/',
})(req, res, next);
}
next(error);
}
};
}
handleRedirect(options = {}) {
return async (req, res, next) => {
if (!req.body || !req.body.state) {
return next(new Error('Error: response not found'));
}
const authCodeRequest = {
...req.session.authCodeRequest,
code: req.body.code,
codeVerifier: req.session.pkceCodes.verifier,
};
try {
const msalInstance = this.getMsalInstance(this.msalConfig);
if (req.session.tokenCache) {
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.successRedirect);
} catch (error) {
next(error);
}
}
}
logout(options = {}) {
return (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
*/
let logoutUri = `${this.msalConfig.auth.authority}/oauth2/v2.0/`;
if (options.postLogoutRedirectUri) {
logoutUri += `logout?post_logout_redirect_uri=${options.postLogoutRedirectUri}`;
}
req.session.destroy(() => {
res.redirect(logoutUri);
});
}
}
/**
* Instantiates a new MSAL ConfidentialClientApplication object
* @param msalConfig: MSAL Node Configuration object
* @returns
*/
getMsalInstance(msalConfig) {
return new msal.ConfidentialClientApplication(msalConfig);
}
/**
* 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
*/
redirectToAuthCodeUrl(authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
return async (req, res, next) => {
// 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,
responseMode: msal.ResponseMode.FORM_POST, // recommended for confidential clients
codeChallenge: req.session.pkceCodes.challenge,
codeChallengeMethod: req.session.pkceCodes.challengeMethod,
};
req.session.authCodeRequest = {
...authCodeRequestParams,
code: '',
};
try {
const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
res.redirect(authCodeUrlResponse);
} catch (error) {
next(error);
}
};
}
/**
* Retrieves cloud discovery metadata from the /discovery/instance endpoint
* @returns
*/
async getCloudDiscoveryMetadata(authority) {
const endpoint = 'https://login.microsoftonline.com/common/discovery/instance';
try {
const response = await axios.get(endpoint, {
params: {
'api-version': '1.1',
'authorization_endpoint': `${authority}/oauth2/v2.0/authorize`
}
});
return await response.data;
} catch (error) {
throw error;
}
}
/**
* Retrieves oidc metadata from the openid endpoint
* @returns
*/
async getAuthorityMetadata(authority) {
const endpoint = `${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);
module.exports = authProvider;
- Después, cree un archivo denominado auth.js en la carpeta rutas y agregue allí el código siguiente:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var express = require('express');
const authProvider = require('../auth/AuthProvider');
const { REDIRECT_URI, POST_LOGOUT_REDIRECT_URI } = require('../authConfig');
const router = express.Router();
router.get('/signin', authProvider.login({
scopes: [],
redirectUri: REDIRECT_URI,
successRedirect: '/'
}));
router.get('/acquireToken', authProvider.acquireToken({
scopes: ['User.Read'],
redirectUri: REDIRECT_URI,
successRedirect: '/users/profile'
}));
router.post('/redirect', authProvider.handleRedirect());
router.get('/signout', authProvider.logout({
postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI
}));
module.exports = router;
- Actualice la ruta de index.js al reemplazar el código existente por el siguiente fragmento de código:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var express = require('express');
var 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,
});
});
module.exports = router;
- Por último, actualice la ruta de users.js al reemplazar el código existente por el siguiente fragmento de código:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var express = require('express');
var router = express.Router();
var fetch = require('../fetch');
var { GRAPH_ME_ENDPOINT } = require('../authConfig');
// 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 });
}
);
router.get('/profile',
isAuthenticated, // check if user is authenticated
async function (req, res, next) {
try {
const graphResponse = await fetch(GRAPH_ME_ENDPOINT, req.session.accessToken);
res.render('profile', { profile: graphResponse });
} catch (error) {
next(error);
}
}
);
module.exports = router;
Adición de código para llamar a Microsoft Graph API
Cree un archivo con el nombre fetch.js en la raíz del proyecto y agregue el código siguiente:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var axios = require('axios');
/**
* Attaches a given access token to a MS Graph API call
* @param endpoint: REST API endpoint to call
* @param accessToken: raw access token string
*/
async function fetch(endpoint, accessToken) {
const options = {
headers: {
Authorization: `Bearer ${accessToken}`
}
};
console.log(`request made to ${endpoint} at: ` + new Date().toString());
try {
const response = await axios.get(endpoint, options);
return await response.data;
} catch (error) {
throw new Error(error);
}
}
module.exports = fetch;
Adición de vistas para mostrar datos
- En la carpeta views, actualice el archivo index.hbs al reemplazar el código existente por el siguiente:
<h1>{{title}}</h1>
{{#if isAuthenticated }}
<p>Hi {{username}}!</p>
<a href="/users/id">View ID token claims</a>
<br>
<a href="/auth/acquireToken">Acquire a token to call the Microsoft Graph API</a>
<br>
<a href="/auth/signout">Sign out</a>
{{else}}
<p>Welcome to {{title}}</p>
<a href="/auth/signin">Sign in</a>
{{/if}}
- Todavía en la misma carpeta, cree otro archivo con el nombre id.hbs para mostrar el contenido del token de identificador de usuario:
<h1>Azure AD</h1>
<h3>ID Token</h3>
<table>
<tbody>
{{#each idTokenClaims}}
<tr>
<td>{{@key}}</td>
<td>{{this}}</td>
</tr>
{{/each}}
</tbody>
</table>
<br>
<a href="https://aka.ms/id-tokens" target="_blank">Learn about claims in this ID token</a>
<br>
<a href="/">Go back</a>
- Por último, cree otro archivo con el nombre profile.hbs para mostrar el resultado de la llamada realizada a Microsoft Graph:
<h1>Microsoft Graph API</h1>
<h3>/me endpoint response</h3>
<table>
<tbody>
{{#each profile}}
<tr>
<td>{{@key}}</td>
<td>{{this}}</td>
</tr>
{{/each}}
</tbody>
</table>
<br>
<a href="/">Go back</a>
Registro de enrutadores y adición de administración de estado
En el archivo app.js de la raíz de la carpeta del proyecto registre las rutas que ha creado anteriormente y agregue compatibilidad de sesión para realizar el seguimiento del estado de autenticación mediante el paquete express-session. Reemplace el código existente ahí por el siguiente fragmento de código:
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
require('dotenv').config();
var path = require('path');
var express = require('express');
var session = require('express-session');
var createError = require('http-errors');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var authRouter = require('./routes/auth');
// initialize express
var app = express();
/**
* Using express-session middleware for persistent user session. Be sure to
* familiarize yourself with available options. Visit: https://www.npmjs.com/package/express-session
*/
app.use(session({
secret: process.env.EXPRESS_SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false, // set this to true on production
}
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');
app.use(logger('dev'));
app.use(express.json());
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/auth', authRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
Prueba del inicio de sesión y llamada a Microsoft Graph
Ha completado la creación de la aplicación y ya está listo para probar la funcionalidad de la aplicación.
- Inicie el servidor web de Node.js mediante la ejecución del siguiente comando desde la raíz de la carpeta del proyecto:
npm start
- Abra una ventana del explorador y vaya a
http://localhost:3000
. Debería ver una página principal:
- Seleccione el vínculo Iniciar sesión. Debería ver la pantalla de inicio de sesión de Microsoft Entra:
- Una vez que escriba las credenciales, debería ver una pantalla de consentimiento que le pide que apruebe los permisos para la aplicación.
- Una vez que dé su consentimiento, se le debe redirigir a la página principal de la aplicación.
- Seleccione el vínculo View ID Token (Ver token de identificador) para mostrar el contenido del token de identificador del usuario que ha iniciado sesión.
- Vuelva a la página principal y seleccione el vínculo Acquire an access token and call the Microsoft Graph API (Adquirir un token de acceso y llamar a Microsoft Graph API). Una vez hecho esto, debería ver la respuesta del punto de conexión /me de Microsoft Graph del usuario que ha iniciado sesión.
- Vuelva a la página principal y seleccione el vínculo Cerrar sesión. Debería ver la pantalla de cierre de sesión de Microsoft Entra.
Cómo funciona la aplicación
En este tutorial ha creado una instancia de un objeto de MSAL Node ConfidentialClientApplication al pasarle un objeto de configuración (msalConfig) que contiene parámetros obtenidos del registro de la aplicación Microsoft Entra en Azure Portal. La aplicación web creada usa el protocolo OpenID Connect para iniciar la sesión de los usuarios y el flujo de código de autorización de OAuth 2.0 para obtener los tokens de acceso.
Pasos siguientes
Si quiere profundizar más en el desarrollo de aplicaciones web de Node.js y Express en la Plataforma de identidad de Microsoft, consulte nuestra serie de escenarios dividida en varias partes: