Como migrar um aplicativo JavaScript do ADAL.js para o MSAL.js

Microsoft Authentication Library for JavaScript (MSAL.js, também conhecido como msal-browser) 2.x é a biblioteca de autenticação que recomendamos usar com aplicativos JavaScript na plataforma de identidade da Microsoft. Este artigo destaca as alterações que você precisa fazer para migrar um aplicativo que usa o ADAL.js para usar o MSAL.js 2.x

Nota

Recomendamos vivamente MSAL.js 2.x sobre 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 do usuário, apesar das medidas de privacidade que navegadores como o Safari implementaram para bloquear cookies de terceiros, entre outros benefícios.

Pré-requisitos

  • Você deve definir o Tipo de URL de Resposta da Plataforma / como Aplicativo de página única no portal de Registro de Aplicativo (se você tiver outras plataformas adicionadas no registro do aplicativo, como a Web, precisará garantir que os URIs de redirecionamento não se sobreponham. Consulte: Restrições de URI de redirecionamento)
  • Você deve fornecer polipreenchimentos para os recursos do ES6 nos quais o 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, se ainda não o tiver feito

Instalar e importar MSAL

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

Via npm:

npm install @azure/msal-browser

Em seguida, dependendo do seu sistema de módulos, importe-o como 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 seu 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 obter links CDN alternativos e práticas recomendadas ao usar CDN, consulte: Uso de CDN

Inicializar MSAL

No ADAL.js, você instancia a classe AuthenticationContext , que expõe os métodos que você pode usar para obter autenticação (login, acquireTokenPopup etc.). Este objeto serve como a representação da conexão do seu aplicativo com o servidor de autorização ou provedor de identidade. Ao inicializar, o único parâmetro obrigatório é o clientId:

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

var authContext = new AuthenticationContext(config);

No MSAL.js, você instancia a classe PublicClientApplication em vez disso. Como ADAL.js, o construtor espera um objeto de configuração que contém o clientId parâmetro no mínimo. Consulte para mais: Inicializar MSAL.js

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

const msalInstance = new msal.PublicClientApplication(msalConfig);

Tanto no ADAL.js quanto no MSAL.js, o URI de autoridade assume https://login.microsoftonline.com/common como padrão se você não o especificar.

Nota

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

Configurar o MSAL

Algumas das opções de configuração no ADAL.js que são usadas ao inicializar AuthenticationContext são preteridas no MSAL.js, enquanto algumas novas são introduzidas. 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, permitindo que você as defina por solicitação . Por exemplo, você pode usar um URIde autoridade ou URI de redirecionamento diferente daquele definido durante a inicialização ao adquirir tokens.

Além disso, você não precisa mais especificar a experiência de login (ou seja, se usando janelas pop-up ou redirecionando a página) através das opções de configuração. Em vez disso, MSAL.js expõe loginPopup e loginRedirect métodos através da PublicClientApplication instância.

Ativar registo

No ADAL.js, você configura o registro 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)

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

Mudar para a API MSAL

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

ADAL MSAL Notas
acquireToken acquireTokenSilent Renomeado e agora espera um objeto de conta
acquireTokenPopup acquireTokenPopup Agora assíncrono e retorna uma promessa
acquireTokenRedirect acquireTokenRedirect Agora assíncrono e retorna uma promessa
handleWindowCallback handleRedirectPromise Necessário se estiver usando a experiência de redirecionamento
getCachedUser getAllAccounts Renomeado e agora retorna uma matriz de contas.

Outros foram preteridos, enquanto o MSAL.js oferece novos métodos:

ADAL MSAL Notas
login N/D Preterido. Utilizar loginPopup ou loginRedirect
logOut N/D Preterido. Utilizar logoutPopup ou logoutRedirect
N/A loginPopup
N/D loginRedirect
N/D logoutPopup
N/D logoutRedirect
N/A getAccountByHomeId Filtra contas por home ID (oid + tenant ID)
N/D getAccountLocalId Filtra contas por ID local (útil para ADFS)
N/D getAccountUsername Filtra contas por nome de utilizador (se existir)

Além disso, como o MSAL.js é implementado no TypeScript ao contrário do ADAL.js, ele expõe vários tipos e interfaces que você pode usar em seus projetos. Consulte a referência da API MSAL.js para obter mais informações.

Usar escopos em vez de recursos

Uma diferença importante entre os pontos de extremidade do Azure Ative Directory v1.0 versus 2.0 é sobre como os recursos são acessados. Ao usar o ADAL.js com o ponto de extremidade v1.0, você primeiro registraria uma permissão no portal de registro de aplicativo e, em seguida, solicitaria 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
});

MSAL.js suporta apenas o ponto de extremidade v2.0 . O ponto de extremidade v2.0 emprega um modelo centrado no escopo para acessar recursos. Assim, quando você solicita um token de acesso para um recurso, você também precisa especificar o escopo para esse 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 usando o ponto de extremidade v1.0, você precisava registrar o conjunto completo de permissões (chamadas de escopos estáticos) exigidas pelo aplicativo para o usuário consentir no momento do login. Na v2.0, você pode usar o parâmetro scope para solicitar as permissões no momento desejado (portanto, escopos dinâmicos). Isso permite que o usuário forneça consentimento incremental para escopos. Então, se no início você quiser apenas que o usuário entre no seu aplicativo e você não precisa de nenhum tipo de acesso, você pode fazê-lo. Se mais tarde você precisar da capacidade de ler o calendário do usuário, você pode solicitar o escopo do calendário nos métodos acquireToken e obter o consentimento do usuário. Veja mais: Recursos e escopos

Use promessas em vez de retornos de chamada

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

No MSAL.js, as promessas são usadas em vez disso:

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 async/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 o ADAL.js, o MSAL.js armazena tokens em cache e outros artefatos de autenticação no armazenamento do navegador, usando a API de armazenamento na Web. Recomenda-se que você use sessionStorage a opção (consulte: configuração) porque ela é mais segura no armazenamento de tokens adquiridos por seus usuários, mas localStorage lhe dará logon único em guias e sessões de usuário.

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

Renovar tokens com tokens de atualização

ADAL.js usa o fluxo implícito OAuth 2.0, que não retorna tokens de atualização por motivos de segurança (os tokens de atualização têm vida útil mais longa do que os tokens de acesso e, portanto, são mais perigosos nas mãos de atores mal-intencionados). Assim, ADAL.js executa a renovação de token usando um IFrame oculto para que o usuário não seja repetidamente solicitado a autenticar.

Com o fluxo de código de autenticação com suporte PKCE, os aplicativos que usam MSAL.js 2.x obtêm tokens de atualização junto com tokens de ID e acesso, que podem ser usados para renová-los. O uso de tokens de atualização é abstraído, e os desenvolvedores não devem construir lógica em torno deles. Em vez disso, o MSAL gerencia a renovação de tokens usando tokens de atualização por si só. Seu cache de token anterior com ADAL.js não será transferível para MSAL.js, pois o esquema de cache de token foi alterado e incompatível com o esquema usado no ADAL.js.

Lidar com erros e exceções

Ao usar o MSAL.js, o tipo mais comum de erro que você pode enfrentar é o interaction_in_progress erro. Este erro é gerado quando uma API interativa (loginPopup, , , loginRedirectacquireTokenPopupacquireTokenRedirect) é invocada enquanto outra API interativa ainda está em andamento. As login* APIs e acquireToken* são assíncronas , portanto, você precisará garantir que as promessas resultantes tenham sido resolvidas antes de invocar outra.

Outro erro comum é interaction_required. Esse erro geralmente é resolvido iniciando 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, exigindo que o usuário execute a autenticação multifator (MFA). Nesse caso, o tratamento do erro acionando acquireTokenPopup ou acquireTokenRedirect solicitará ao usuário MFA, permitindo que ele o interaction_required preencha.

Outro erro comum que você pode enfrentar é consent_requiredo , 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 no interaction_required, a solução para consent_required o erro geralmente é iniciar um prompt de aquisição de token interativo, usando um ou acquireTokenPopupacquireTokenRedirect.

Veja mais: Erros comuns do MSAL.js e como lidar com eles

Usar a API de eventos

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 interface do usuário, mostrar mensagens de erro, verificar se alguma interação está em andamento e assim por diante. Por exemplo, abaixo está um retorno de chamada de evento que será chamado quando o processo de login 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 o desempenho, é importante cancelar o registro de retornos de chamada de eventos quando eles não forem mais necessários. Veja mais: MSAL.js Events API

Lidar com várias contas

ADAL.js tem o conceito de um usuário para representar a entidade atualmente autenticada. MSAL.js substitui os usuários por contas, dado o fato de 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 a apropriada para trabalhar. O trecho 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 obter mais informações, consulte: Contas no MSAL.js

Usar as bibliotecas de wrappers

Se você estiver desenvolvendo para estruturas Angular e React, poderá usar o MSAL Angular v2 e o MSAL React, respectivamente. Esses wrappers expõem a mesma API pública do MSAL.js ao mesmo tempo em que oferecem métodos e componentes específicos da estrutura que podem simplificar os processos de autenticação e aquisição de tokens.

Executar a aplicação

Quando as alterações estiverem concluídas, execute o aplicativo e teste seu cenário de autenticação:

npm start

Exemplo: Proteger um SPA com ADAL.js vs. MSAL.js

Os trechos abaixo demonstram o código mínimo necessário para um aplicativo de página única autenticando usuários com a plataforma de identidade da Microsoft e obtendo um token de acesso para o Microsoft Graph usando primeiro ADAL.js e depois MSAL.js:

Usando a ADAL.js Usando o 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óximos passos