Como migrar um aplicativo JavaScript da ADAL.js para a MSAL.js

A MSAL.js (Biblioteca de Autenticação da Microsoft para JavaScript) 2.x, também conhecida como msal-browser, é a biblioteca de autenticação recomendada para aplicativos JavaScript na plataforma de identidade da Microsoft. Este artigo destaca as alterações necessárias para migrar um aplicativo que usa a ADAL.js para a MSAL.js 2.x

Observação

É recomendado usar a MSAL.js 2.x em vez da MSAL.js 1.x. O fluxo de concessão de código de autenticação é mais seguro e permite que aplicativos de página única mantenham uma boa experiência de usuário, apesar das medidas de privacidade que navegadores como o Safari implementaram para bloquear cookies de terceiros, entre outros benefícios.

Pré-requisitos

  • Configure Plataforma / Tipo de URL de Resposta como Aplicativo de página única no portal de Registro de Aplicativo (se você tiver outras plataformas adicionadas ao registro de aplicativo, como Web, verifique se os URIs de redirecionamento não se sobrepõem. Confira: Restrições de URI de redirecionamento)
  • Forneça o polyfills aos recursos ES6 dos quais a MSAL.js depende (por exemplo, promessas) para executar seus aplicativos no Internet Explorer
  • Migre seus aplicativos do Microsoft Entra para o ponto de extremidade v2, caso ainda não tenha feito isso

Instalar e importar MSAL

Há duas maneiras de instalar a biblioteca MSAL.js 2.x:

Via npm:

npm install @azure/msal-browser

Neste caso, dependendo do sistema de módulos, importe-o conforme mostrado abaixo:

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

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

Via CDN:

Carregue o script na seção de cabeçalho do 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 ver links alternativos sobre CDN e práticas recomendadas ao usá-la, veja Uso da CDN

Inicializar a MSAL

Na ADAL.js, a classe AuthenticationContext é instanciada e, em seguida, expõe os métodos a serem usados para obter a autenticação (login, acquireTokenPopup etc.). Este objeto atua como a representação da conexão do aplicativo com o servidor de autorização ou o provedor de identidade. Ao inicializar, o único parâmetro obrigatório é clientId:

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

var authContext = new AuthenticationContext(config);

Na MSAL.js, você instancia a classe PublicClientApplication. Como a ADAL.js, o construtor espera um objeto de configuração que contenha no mínimo o parâmetro clientId. Para saber mais,veja Inicializar a MSAL.js

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

const msalInstance = new msal.PublicClientApplication(msalConfig);

Na ADAL.js e na MSAL.js, o URI de autoridade é padronizado para https://login.microsoftonline.com/common quando não é especificado.

Observação

Se você usar a autoridade https://login.microsoftonline.com/common na v2.0, permitirá que os usuários entrem com qualquer conta Microsoft pessoal (MSA) ou da organização do Microsoft Entra. Na MSAL.js, para restringir o logon a qualquer conta do Microsoft Entra (mesmo comportamento da ADAL.js), use https://login.microsoftonline.com/organizations.

Configurar a MSAL

Algumas das opções de configuração na ADAL.js que são usadas ao inicializar AuthenticationContext estão preteridas na MSAL.js, mas foram introduzidas outras novas. Veja a lista completa de opções disponíveis. É importante ressaltar que muitas dessas opções, exceto clientId, podem ser substituídas durante a aquisição de tokens, o que permite que você as defina por solicitação. Por exemplo, é possível usar um URI de autoridade ou um URI de redirecionamento diferente daquele definido durante a inicialização ao adquirir tokens.

Além disso, não é necessário especificar a experiência de logon (ou seja, se ela acontece usando janelas pop-up ou redirecionando a página) nas opções de configuração. Em vez disso, a MSAL.js expõe os métodos loginPopup e loginRedirect por meio da instância PublicClientApplication.

Habilitar o registro em log

Na ADAL.js, você configura o log separadamente em qualquer lugar do 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)

Na MSAL.js, o log faz parte das opções de configuração e é criado durante a inicialização 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);

Alternar para a API da MSAL

Alguns dos métodos públicos na ADAL.js têm equivalentes na MSAL.js:

ADAL MSAL Observações
acquireToken acquireTokenSilent Renomeado e agora espera um objeto account
acquireTokenPopup acquireTokenPopup Agora assíncrono e retorna uma promessa
acquireTokenRedirect acquireTokenRedirect Agora assíncrono e retorna uma promessa
handleWindowCallback handleRedirectPromise Necessário ao usar a experiência de redirecionamento
getCachedUser getAllAccounts Renomeado e agora retorna uma matriz de contas.

Embora os outros tenham sido preteridos, a MSAL.js oferece novos métodos:

ADAL MSAL Observações
login N/D Preterido. Use loginPopup ou loginRedirect
logOut N/D Preterido. Use logoutPopup ou logoutRedirect
N/D loginPopup
N/D loginRedirect
N/D logoutPopup
N/D logoutRedirect
N/D getAccountByHomeId Filtra contas por ID da página inicial (oid + ID do locatário)
N/D getAccountLocalId Filtra contas por ID local (útil para ADFS)
N/D getAccountUsername Filtra contas por nome de usuário (se existir)

Além disso, como a MSAL.js é implementada em TypeScript, ao contrário da ADAL.js, ela expõe diversos tipos e interfaces para uso em seus projetos. Confira a Referência da API do MSAL.js para saber mais.

Use escopos em vez de recursos

Uma diferença importante entre os pontos de extremidade do Azure Active Directory v1.0 quando comparado ao 2.0 diz respeito à maneira de acessar os recursos. Ao usar a ADAL.js com o ponto de extremidade da v1.0, primeiro você registrava uma permissão no portal de registro de aplicativo e, em seguida, solicitava um token de acesso para um recurso (como o Microsoft Graph), conforme mostrado abaixo:

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

O MSAL.js só dá suporte ao ponto de extremidade v2.0. O ponto de extremidade da v2.0 emprega um modelo centrado no escopo para acessar recursos. Portanto, quando você solicita um token de acesso para um recurso, também precisará especificar o escopo desse recurso:

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

Uma vantagem do modelo centrado no escopo é a capacidade de usar escopos dinâmicos. Ao criar aplicativos com o ponto de extremidade da v1.0, era preciso registrar o conjunto completo de permissões (chamadas de escopos estáticos) exigidas pelo aplicativo para o consentimento do usuário durante o logon. Na v2.0, você pode usar o parâmetro de escopo para solicitar as permissões no momento em que desejá-las (por isso escopos dinâmicos). Esses escopos permitem que o usuário forneça consentimento incremental aos escopos. Portanto, se no início você só quiser que o usuário entre em seu aplicativo e não precise de nenhum tipo de acesso, poderá fazê-lo. Se, posteriormente, você precisar que o usuário consiga ler o calendário, solicite o escopo do calendário nos métodos de token de aquisição e obtenha o consentimento do usuário. Para mais informações confira: Recursos e escopos

Usar promessas em vez de retornos de chamada

Na ADAL.js, os retornos de chamada são usados ​​para qualquer operação depois que a autenticação é bem-sucedida e uma resposta é obtida:

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

Na MSAL.js, são usadas promessas:

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

Você também pode usar a sintaxe assíncrona/await que vem com o ES8:

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

Armazenar em cache e recuperar tokens

Como a ADAL.js, a MSAL.js armazena em cache tokens e outros artefatos de autenticação no armazenamento do navegador, usando a API de armazenamento na Web. Recomenda-se usar a opção sessionStorage (veja: configuração), que é mais segura para o armazenamento de tokens adquiridos por seus usuários, mas localStorage fornecerá o logon único em guias e sessões de usuário.

É importante ressaltar que você não deve acessar o cache diretamente. Você deve usar uma API apropriada da MSAL.js para recuperar artefatos de autenticação, como tokens de acesso ou contas de usuário.

Renovar tokens com tokens de atualização

A ADAL.js usa o fluxo implícito do OAuth 2.0, que não retorna tokens de atualização por motivos de segurança (os tokens de atualização têm tempo de vida maior do que os de acesso e, portanto, são mais perigosos nas mãos de agentes mal-intencionados). Portanto, a ADAL.js executa a renovação do token usando um IFrame oculto para que não seja repetidamente solicitado ao usuário que ele se autentique.

Com o fluxo de código de autenticação com suporte a PKCE, os aplicativos que usam a MSAL.js 2.x obtêm tokens de atualização com ID e tokens de acesso, que podem ser usados ​​para renová-los. O uso de tokens de atualização é abstraído e os desenvolvedores não devem criar uma lógica sobre eles. A MSAL gerencia a renovação de tokens usando os tokens de atualização por si só. Seu cache de token anterior com a ADAL.js não será transferível para a MSAL.js, pois o esquema de cache de token mudou e é incompatível com o esquema usado na ADAL.js.

Lidar com erros e exceções

Ao usar a MSAL.js, o erro interaction_in_progress é o tipo mais comum. Esse erro é lançado quando uma API interativa (loginPopup, loginRedirect, acquireTokenPopup, acquireTokenRedirect) é invocada enquanto outra ainda está em andamento. As APIs login* e acquireToken* são assíncronas, portanto, é necessário garantir que as promessas resultantes foram resolvidas antes de chamar outra.

Outro erro comum é interaction_required. Ele geralmente é resolvido com a inicialização de um prompt de aquisição de token interativo. Por exemplo, a API da Web que você está tentando acessar pode ter uma política de Acesso Condicional em vigor, que pode exigir que o usuário execute a autenticação multifator (MFA). Nesse caso, lidar com o erro interaction_required disparando acquireTokenPopup ou acquireTokenRedirectsolicitará ao usuário a MFA, o que permite que ele a realize.

Outro erro comum que você pode enfrentar é consent_required, que ocorre quando as permissões necessárias para obter um token de acesso para um recurso protegido não são consentidas pelo usuário. Como em interaction_required, a solução para o erro consent_required geralmente é iniciar um prompt de aquisição de token interativo, usando acquireTokenPopup ou acquireTokenRedirect.

Saiba mais em Erros comuns da MSAL.js e como lidar com eles

Usar a API de Eventos

A MSAL.js (>=v2.4) apresenta uma API de Eventos que você pode usar em seus aplicativos. Esses eventos estão relacionados ao processo de autenticação e ao que a MSAL está fazendo a qualquer momento, e podem ser usados ​​para atualizar a IU, mostrar mensagens de erro, verificar se alguma interação está em andamento e assim por diante. Por exemplo, veja abaixo um retorno de chamada de evento que será chamado quando o processo de logon falhar por qualquer 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 fins de desempenho, é importante cancelar o registro dos retornos de chamada de evento quando eles não forem mais necessários. Veja mais: API de Eventos da MSAL.js

Lidar com diversas contas

A ADAL.js tem o conceito de um usuário para representar a entidade autenticada atualmente. A MSAL.js substitui usuários por contas, visto que um usuário pode ter mais de uma conta associada a eles. Isso também significa que agora você precisa controlar várias contas e escolher aquela com a qual trabalhar. O snippet abaixo ilustra esse processo:

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 saber mais, confira: Contas na MSAL.js

Usar as bibliotecas de wrappers

Se você está desenvolvendo para estruturas Angular e React, é possível usar o MSAL Angular v2 e o MSAL React, respectivamente. Esses wrappers expõem a mesma API pública da MSAL.js, oferecendo métodos e componentes específicos da estrutura que podem agilizar os processos de autenticação e aquisição de tokens.

Executar o aplicativo

Depois que as alterações forem feitas, execute o aplicativo e teste seu cenário de autenticação:

npm start

Exemplo: protegendo um SPA com ADAL.js versus MSAL.js

Os snippets abaixo demonstram o código mínimo necessário para que um aplicativo de página única autentique usuários com a plataforma de identidade da Microsoft e obtenha um token de acesso para o Microsoft Graph usando primeiro a ADAL.js e, em seguida, a MSAL.js:

Usando a ADAL.js Usando a 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>

Próximas etapas