Cómo migrar una aplicación de JavaScript de ADAL.js a MSAL.js

La biblioteca de autenticación de Microsoft para JavaScript (MSAL.js, también conocida como msal-browser) 2.x es la biblioteca de autenticación que recomendamos utilizar con aplicaciones JavaScript en la plataforma de identidad de Microsoft. En este artículo se resaltan los cambios que debe realizar para migrar una aplicación que usa ADAL.js para usar MSAL.js 2.x.

Nota:

Se recomienda encarecidamente usar MSAL.js 2.x en lugar de MSAL.js 1.x. El flujo de concesión de código de autenticación es más seguro y permite que las aplicaciones de página única mantengan una buena experiencia de usuario a pesar de las medidas de privacidad que los exploradores como Safari han implementado para bloquear las cookies de terceros, entre otras ventajas.

Requisitos previos

  • Debe establecer el Tipo de plataforma / URL de respuesta en Aplicación de página única en el portal de registro de aplicaciones (si tiene otras plataformas en su registro de aplicaciones, como Web, debe asegurarse de que los URI de redirección no se solapen; consulte Restricciones del URI de redirección).
  • Debe proporcionar polyfills para las características de ES6 en las que se basa MSAL.js (por ejemplo, promesas) para ejecutar sus aplicaciones en Internet Explorer
  • Migre las aplicaciones de Microsoft Entra al punto de conexión v2 si aún no lo ha hecho

Instalación e importación de MSAL

Hay dos maneras de instalar la biblioteca MSAL.js 2.x:

A través de npm:

npm install @azure/msal-browser

A continuación, en función del sistema del módulo, impórtelo como se muestra a continuación:

import * as msal from "@azure/msal-browser"; // ESM

const msal = require('@azure/msal-browser'); // CommonJS

A través de CDN:

Cargue el script en la sección de encabezado del documento HTML:

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js"></script>
  </head>
</html>

Para obtener vínculos alternativos de CDN y los procedimientos recomendados para utilizarlos, consulte Uso de CDN.

Inicializar MSAL

En ADAL.js, se crea una instancia de la clase AuthenticationContext, que luego expone los métodos que puede usar para lograr la autenticación (login, acquireTokenPopup etc.). Este objeto actúa como la representación de la conexión de la aplicación con el servidor de autorización o el proveedor de identidades. Al inicializarse, el único parámetro obligatorio es clientId:

window.config = {
  clientId: "YOUR_CLIENT_ID"
};

var authContext = new AuthenticationContext(config);

En MSAL.js, se crea una instancia de la clase PublicClientApplication en su lugar. Al igual que ADAL.js, el constructor espera un objeto de configuración que contiene el parámetro clientId como mínimo. Para obtener más información, consulte Inicialización de MSAL.js.

const msalConfig = {
  auth: {
      clientId: 'YOUR_CLIENT_ID'
  }
};

const msalInstance = new msal.PublicClientApplication(msalConfig);

Tanto en ADAL.js como en MSAL.js, el URI de autoridad tiene como valor predeterminado https://login.microsoftonline.com/common si no se especifica.

Nota:

Si utiliza la https://login.microsoftonline.com/common autoridad en v2.0, permitirá a los usuarios iniciar sesión con cualquier organización de Microsoft Entra o una cuenta personal de Microsoft (MSA). En MSAL.js, si desea restringir el inicio de sesión a cualquier cuenta de Microsoft Entra (el mismo comportamiento que con ADAL.js), utilice https://login.microsoftonline.com/organizations en su lugar.

Configuración de MSAL

Algunas de las opciones de configuración de ADAL.js que se utilizan al inicializar AuthenticationContext dejan de usarse en MSAL.js, mientras que se incluyen algunas nuevas. Vea la lista completa de opciones disponibles. Lo importante es que muchas de estas opciones, excepto clientId, pueden invalidarse durante la adquisición de tokens, lo que permite establecerlas en función de cada solicitud. Por ejemplo, puede utilizar un URI de autoridad o un URI de redirección diferente al que estableció durante la inicialización al adquirir los tokens.

Además, ya no es necesario especificar la experiencia de inicio de sesión (es decir, si se usan ventanas emergentes o se redirige la página) mediante las opciones de configuración. En cambio, MSAL.js expone los métodos loginPopup y loginRedirect mediante la instancia PublicClientApplication.

Habilitar registro

En ADAL.js, configure el registro por separado en cualquier lugar del código:

window.config = {
  clientId: "YOUR_CLIENT_ID"
};

var authContext = new AuthenticationContext(config);

var Logging = {
  level: 3,
  log: function (message) {
      console.log(message);
  },
  piiLoggingEnabled: false
};


authContext.log(Logging)

En MSAL.js, el registro forma parte de las opciones de configuración y se crea durante la inicialización de PublicClientApplication:

const msalConfig = {
  auth: {
      // authentication related parameters
  },
  cache: {
      // cache related parameters
  },
  system: {
      loggerOptions: {
          loggerCallback(loglevel, message, containsPii) {
              console.log(message);
          },
          piiLoggingEnabled: false,
          logLevel: msal.LogLevel.Verbose,
      }
  }
}

const msalInstance = new msal.PublicClientApplication(msalConfig);

Cambio a la API de MSAL

La mayoría de los métodos públicos de ADAL.js tienen equivalentes en MSAL.js:

ADAL MSAL Notas
acquireToken acquireTokenSilent Se ha cambiado el nombre y ahora espera un objeto de cuenta.
acquireTokenPopup acquireTokenPopup Ahora asincrónico y devuelve una promesa.
acquireTokenRedirect acquireTokenRedirect Ahora asincrónico y devuelve una promesa.
handleWindowCallback handleRedirectPromise Necesario si se usa la experiencia de redirección.
getCachedUser getAllAccounts Se ha cambiado el nombre y ahora devuelve una matriz de cuentas.

Otros quedaron en desuso, mientras que MSAL.js ofrece nuevos métodos:

ADAL MSAL Notas
login N/D En desuso. Utilice loginPopup o loginRedirect.
logOut N/D En desuso. Utilice logoutPopup o logoutRedirect.
N/D loginPopup
N/D loginRedirect
N/D logoutPopup
N/D logoutRedirect
N/D getAccountByHomeId Filtra las cuentas por identificador de inicio (oid + id. de inquilino).
N/D getAccountLocalId Filtra las cuentas por identificador local (útil para ADFS)
N/D getAccountUsername Filtra las cuentas por nombre de usuario (si existe).

Además, como MSAL.js se implementa en TypeScript a diferencia de ADAL.js, expone varios tipos e interfaces que puede usar en los proyectos. Consulte la referencia de la API de MSAL.js para obtener más detalles.

Uso de ámbitos en lugar de recursos

Una diferencia importante entre los puntos de conexión de Azure Active Directory v1.0 y 2.0 es sobre cómo se accede a los recursos. Al utilizar ADAL.js con el punto de conexión v1.0, primero registraría un permiso en el portal de registro de aplicaciones y, a continuación, solicitaría un token de acceso para un recurso (como Microsoft Graph), como se muestra a continuación:

authContext.acquireTokenRedirect("https://graph.microsoft.com", function (error, token) {
  // do something with the access token
});

MSAL.js solo admite el punto de conexión v2.0. El punto de conexión v2.0 emplea un modelo centrado en el ámbito para acceder a los recursos. Por lo tanto, cuando solicite un token de acceso para un recurso, también debe especificar el ámbito de ese recurso:

msalInstance.acquireTokenRedirect({
  scopes: ["https://graph.microsoft.com/User.Read"]
});

Una ventaja del modelo centrado en el ámbito es la posibilidad de usar ámbitos dinámicos. Al compilar aplicaciones con el punto de conexión v1.0, era necesario registrar el conjunto completo de permisos (llamados ámbitos estáticos) que requería la aplicación para que el usuario diera su consentimiento en el momento del inicio de sesión. En v2.0, puede usar el parámetro de ámbito para solicitar los permisos en el momento en que lo desee (por eso se llaman ámbitos dinámicos). Esto permite que el usuario dé su consentimiento incremental a los ámbitos. Por lo tanto, si al inicio solo quería que el usuario iniciara sesión en la aplicación y no necesita ningún tipo de acceso, puede hacerlo. Si posteriormente necesita poder leer el calendario del usuario, podrá solicitar el ámbito de calendario en los métodos acquireToken y obtener el consentimiento del usuario. Para obtener más información, consulte Recursos y ámbitos.

Uso de promesas en lugar de devoluciones de llamada

En ADAL.js, las devoluciones de llamada se usan para cualquier operación después de que la autenticación se realice correctamente y se obtenga una respuesta:

authContext.acquireTokenPopup(resource, extraQueryParameter, claims, function (error, token) {
  // do something with the access token
});

En MSAL.js, se usan promesas:

msalInstance.acquireTokenPopup({
      scopes: ["User.Read"] // shorthand for https://graph.microsoft.com/User.Read
  }).then((response) => {
      // do something with the auth response
  }).catch((error) => {
      // handle errors
  });

También puede usar la sintaxis async/await que se incluye con ES8:

const getAccessToken = async() => {
  try {
      const authResponse = await msalInstance.acquireTokenPopup({
          scopes: ["User.Read"]
      });
  } catch (error) {
      // handle errors
  }
}

Almacenamiento en caché y recuperación de tokens

Al igual que ADAL.js, MSAL.js almacena en caché los tokens y otros artefactos de autenticación en el almacenamiento del explorador, utilizando la API de almacenamiento web. Se recomienda usar la opción sessionStorage (consulte Configuración) porque es más segura para almacenar los tokens que adquieren sus usuarios, pero localStorage le proporcionará inicio de sesión único entre pestañas y sesiones de usuario.

Es importante destacar que no se debe acceder a la caché directamente. En su lugar, debe utilizar una API de MSAL.js adecuada para recuperar artefactos de autenticación como tokens de acceso o cuentas de usuario.

Renovación de tokens con tokens de actualización

ADAL.js usa el flujo implícito de OAuth 2.0, que no devuelve tokens de actualización por razones de seguridad (los tokens de actualización tienen una duración más larga que los tokens de acceso y, por tanto, son más peligrosos en manos de actores malintencionados). Por lo tanto, ADAL.js realiza la renovación de los tokens mediante un iframe oculto para que no se le pida al usuario que se autentique repetidamente.

Con el flujo de código de autenticación compatible con PKCE, las aplicaciones que utilizan MSAL.js 2.x obtienen tokens de actualización junto con tokens de identificación y acceso, que se pueden usar para renovarlos. El uso de tokens de actualización se abstrae, y se supone que los desarrolladores no deben crear lógica en torno a ellos. En su lugar, MSAL administra la renovación de tokens mediante tokens de actualización por sí mismo. La caché de tokens anterior con ADAL.js no será transferible a MSAL.js, ya que el esquema de caché de tokens ha cambiado y es incompatible con el esquema que se usa en ADAL.js.

Control de errores y excepciones

Al usar MSAL.js, el tipo de error más común al que podría enfrentarse es el error interaction_in_progress. Este error se produce cuando se invoca una API interactiva (loginPopup, loginRedirect, acquireTokenPopup, acquireTokenRedirect) mientras otra API interactiva sigue en curso. Las API login* y acquireToken* son asincrónicas, por lo que deberá asegurarse de que las promesas resultantes se han resuelto antes de invocar otra.

Otro error común es interaction_required. Para resolverlo, en general hay que iniciar un símbolo del sistema de adquisición de tokens interactivo. Por ejemplo, la API web a la que intenta acceder podría tener una directiva de acceso condicional, que requiere que el usuario realice la autenticación multifactor (MFA). En ese caso, el control del error interaction_required mediante el desencadenamiento de acquireTokenPopup o acquireTokenRedirect solicitará al usuario que realice la autenticación multifactor, lo que le permitirá completarla.

Otro error común al que podría enfrentarse es consent_required, que se produce cuando el usuario no da su consentimiento a los permisos necesarios para obtener un token de acceso para un recurso protegido. Al igual que en el caso de interaction_required, la solución del error consent_required suele consistir en iniciar un símbolo del sistema de adquisición de tokens interactivo mediante el método acquireTokenPopup o acquireTokenRedirect.

Más información: Errores comunes de MSAL.js y solución

Uso de la API de eventos

MSAL.js (>=v2.4) presenta una API de eventos que se puede usar en las aplicaciones. Estos eventos están relacionados con el proceso de autenticación y lo que hace MSAL en cualquier momento, y se pueden usar para actualizar la interfaz de usuario, mostrar mensajes de error, comprobar si hay alguna interacción en curso, etc. Por ejemplo, a continuación se muestra una devolución de llamada de evento a la que se llamará cuando se produce un error en el proceso de inicio de sesión por cualquier motivo:

const callbackId = msalInstance.addEventCallback((message) => {
  // Update UI or interact with EventMessage here
  if (message.eventType === EventType.LOGIN_FAILURE) {
      if (message.error instanceof AuthError) {
          // Do something with the error
      }
    }
});

Para mejorar el rendimiento, es importante anular el registro de las devoluciones de llamada de eventos cuando ya no sean necesarias. Más información: API de eventos de MSAL.js

Control de varias cuentas

ADAL.js tiene el concepto de usuario para representar la entidad autenticada actualmente. MSAL.js sustituye usuarios por cuentas, dado que un usuario puede tener más de una cuenta asociada. Esto también significa que ahora debe controlar varias cuentas y elegir la adecuada con la que trabajar. El siguiente fragmento de código ilustra este proceso:

let homeAccountId = null; // Initialize global accountId (can also be localAccountId or username) used for account lookup later, ideally stored in app state

// This callback is passed into `acquireTokenPopup` and `acquireTokenRedirect` to handle the interactive auth response
function handleResponse(resp) {
  if (resp !== null) {
      homeAccountId = resp.account.homeAccountId; // alternatively: resp.account.homeAccountId or resp.account.username
  } else {
      const currentAccounts = myMSALObj.getAllAccounts();
      if (currentAccounts.length < 1) { // No cached accounts
          return;
      } else if (currentAccounts.length > 1) { // Multiple account scenario
          // Add account selection logic here
      } else if (currentAccounts.length === 1) {
          homeAccountId = currentAccounts[0].homeAccountId; // Single account scenario
      }
  }
}

Para más información, consulte Cuentas en MSAL.js.

Uso de bibliotecas contenedoras

Si está desarrollando para marcos Angular y React, puede usar MSAL Angular v2 y MSAL React, respectivamente. Estos contenedores exponen la misma API pública que MSAL.js mientras ofrecen métodos y componentes específicos del marco que pueden simplificar los procesos de autenticación y adquisición de tokens.

Ejecución la aplicación

Una vez realizados los cambios, ejecute la aplicación y pruebe el escenario de autenticación:

npm start

Ejemplo: Protección de una SPA con ADAL.js frente a MSAL.js

Los siguientes fragmentos de código demuestran el código mínimo necesario para una aplicación de página única que autentica a los usuarios con la plataforma de identidad de Microsoft y obtiene un token de acceso para Microsoft Graph utilizando primero ADAL.js y luego MSAL.js:

Uso de ADAL.js Uso de MSAL.js

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="https://alcdn.msauth.net/lib/1.0.18/js/adal.min.js"></script>
</head>

<body>
    <div>
        <p id="welcomeMessage" style="visibility: hidden;"></p>
        <button id="loginButton">Login</button>
        <button id="logoutButton" style="visibility: hidden;">Logout</button>
        <button id="tokenButton" style="visibility: hidden;">Get Token</button>
    </div>
    <script>
        // DOM elements to work with
        var welcomeMessage = document.getElementById("welcomeMessage");
        var loginButton = document.getElementById("loginButton");
        var logoutButton = document.getElementById("logoutButton");
        var tokenButton = document.getElementById("tokenButton");

        // if user is logged in, update the UI
        function updateUI(user) {
            if (!user) {
                return;
            }

            welcomeMessage.innerHTML = 'Hello ' + user.profile.upn + '!';
            welcomeMessage.style.visibility = "visible";
            logoutButton.style.visibility = "visible";
            tokenButton.style.visibility = "visible";
            loginButton.style.visibility = "hidden";
        };

        // attach logger configuration to window
        window.Logging = {
            piiLoggingEnabled: false,
            level: 3,
            log: function (message) {
                console.log(message);
            }
        };

        // ADAL configuration
        var adalConfig = {
            instance: 'https://login.microsoftonline.com/',
            clientId: "ENTER_CLIENT_ID_HERE",
            tenant: "ENTER_TENANT_ID_HERE",
            redirectUri: "ENTER_REDIRECT_URI_HERE",
            cacheLocation: "sessionStorage",
            popUp: true,
            callback: function (errorDesc, token, error, tokenType) {
                if (error) {
                    console.log(error, errorDesc);
                } else {
                    updateUI(authContext.getCachedUser());
                }
            }
        };

        // instantiate ADAL client object
        var authContext = new AuthenticationContext(adalConfig);

        // handle redirect response or check for cached user
        if (authContext.isCallback(window.location.hash)) {
            authContext.handleWindowCallback();
        } else {
            updateUI(authContext.getCachedUser());
        }

        // attach event handlers to button clicks
        loginButton.addEventListener('click', function () {
            authContext.login();
        });

        logoutButton.addEventListener('click', function () {
            authContext.logOut();
        });

        tokenButton.addEventListener('click', () => {
            authContext.acquireToken(
                "https://graph.microsoft.com",
                function (errorDesc, token, error) {
                    if (error) {
                        console.log(error, errorDesc);

                        authContext.acquireTokenPopup(
                            "https://graph.microsoft.com",
                            null, // extraQueryParameters
                            null, // claims
                            function (errorDesc, token, error) {
                                if (error) {
                                    console.log(error, errorDesc);
                                } else {
                                    console.log(token);
                                }
                            }
                        );
                    } else {
                        console.log(token);
                    }
                }
            );
        });
    </script>
</body>

</html>


<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.34.0/js/msal-browser.min.js"></script>
</head>

<body>
    <div>
        <p id="welcomeMessage" style="visibility: hidden;"></p>
        <button id="loginButton">Login</button>
        <button id="logoutButton" style="visibility: hidden;">Logout</button>
        <button id="tokenButton" style="visibility: hidden;">Get Token</button>
    </div>
    <script>
        // DOM elements to work with
        const welcomeMessage = document.getElementById("welcomeMessage");
        const loginButton = document.getElementById("loginButton");
        const logoutButton = document.getElementById("logoutButton");
        const tokenButton = document.getElementById("tokenButton");

        // if user is logged in, update the UI
        const updateUI = (account) => {
            if (!account) {
                return;
            }

            welcomeMessage.innerHTML = `Hello ${account.username}!`;
            welcomeMessage.style.visibility = "visible";
            logoutButton.style.visibility = "visible";
            tokenButton.style.visibility = "visible";
            loginButton.style.visibility = "hidden";
        };

        // MSAL configuration
        const msalConfig = {
            auth: {
                clientId: "ENTER_CLIENT_ID_HERE",
                authority: "https://login.microsoftonline.com/ENTER_TENANT_ID_HERE",
                redirectUri: "ENTER_REDIRECT_URI_HERE",
            },
            cache: {
                cacheLocation: "sessionStorage"
            },
            system: {
                loggerOptions: {
                    loggerCallback(loglevel, message, containsPii) {
                        console.log(message);
                    },
                    piiLoggingEnabled: false,
                    logLevel: msal.LogLevel.Verbose,
                }
            }
        };

        // instantiate MSAL client object
        const pca = new msal.PublicClientApplication(msalConfig);

        // handle redirect response or check for cached user
        pca.handleRedirectPromise().then((response) => {
            if (response) {
                pca.setActiveAccount(response.account);
                updateUI(response.account);
            } else {
                const account = pca.getAllAccounts()[0];
                updateUI(account);
            }
        }).catch((error) => {
            console.log(error);
        });

        // attach event handlers to button clicks
        loginButton.addEventListener('click', () => {
            pca.loginPopup().then((response) => {
                pca.setActiveAccount(response.account);
                updateUI(response.account);
            })
        });

        logoutButton.addEventListener('click', () => {
            pca.logoutPopup().then((response) => {
                window.location.reload();
            });
        });

        tokenButton.addEventListener('click', () => {
            const account = pca.getActiveAccount();

            pca.acquireTokenSilent({
                account: account,
                scopes: ["User.Read"]
            }).then((response) => {
                console.log(response);
            }).catch((error) => {
                if (error instanceof msal.InteractionRequiredAuthError) {
                    pca.acquireTokenPopup({
                        scopes: ["User.Read"]
                    }).then((response) => {
                        console.log(response);
                    });
                }

                console.log(error);
            });
        });
    </script>
</body>

</html>

Pasos siguientes