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

A MSAL Node (Biblioteca de Autenticação da Microsoft para Node) agora é o SDK recomendado para habilitar a autenticação e a autorização para os aplicativos registrados na plataforma de identidade da Microsoft. Este artigo aborda as etapas importantes que você precisa seguir para migrar os aplicativos da ADAL Node (Biblioteca de Autenticação do Active Directory para Node) para a MSAL Node.

Pré-requisitos

Atualizar configurações de registro de aplicativo

Ao trabalhar com o ADAL Node, você provavelmente usava o ponto de extremidade do Azure AD v1.0. Os aplicativos em migração da ADAL para a MSAL devem mudar para o ponto de extremidade do Azure AD v2.0.

Instalar e importar MSAL

  1. Instale o pacote MSAL Node via o npm:
  npm install @azure/msal-node
  1. Depois, importe MSAL Node para o código:
  const msal = require('@azure/msal-node');
  1. Por fim, desinstale o pacote ADAL Node e remova todas as referências no código:
  npm uninstall adal-node

Inicializar a MSAL

No ADAL Node, você inicializa um objeto AuthenticationContext, que expõe os métodos que podem ser usados em diferentes fluxos de autenticação (por exemplo, acquireTokenWithAuthorizationCode para aplicativos Web). Ao inicializar, o único parâmetro obrigatório é o URI de autoridade:

var adal = require('adal-node');

var authorityURI = "https://login.microsoftonline.com/common";
var authenticationContex = new adal.AuthenticationContext(authorityURI);

No MSAL Node, você tem duas alternativas: se estiver compilando um aplicativo móvel ou um aplicativo da área de trabalho, criará uma instância de um objeto PublicClientApplication. O construtor espera um objeto de configuração que contenha pelo menos o parâmetro clientId. A MSAL assume como padrão o URI de autoridade de https://login.microsoftonline.com/common se você não especificá-lo.

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

const pca = new msal.PublicClientApplication({
        auth: {
            clientId: "YOUR_CLIENT_ID"
        }
    });

Observação

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

Por outro lado, se estiver compilando um aplicativo Web ou um aplicativo daemon, você deve criar uma instância de objeto ConfidentialClientApplication. Com esses aplicativos, você também precisa fornecer uma credencial de cliente, como um segredo do cliente ou um certificado:

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

const cca = new msal.ConfidentialClientApplication({
        auth: {
            clientId: "YOUR_CLIENT_ID",
            clientSecret: "YOUR_CLIENT_SECRET"
        }
    });

Tanto PublicClientApplication quanto ConfidentialClientApplication, ao contrário do AuthenticationContext da ADAL, está vinculado a uma ID do cliente. Isso significa que, se tiver IDs do cliente diferentes que gostaria de usar no aplicativo, precisará criar uma nova instância da MSAL para cada uma. Para mais informações confira: Inicialização do MSAL Node

Configurar a MSAL

Ao compilar aplicativos na plataforma de identidade da Microsoft, o aplicativo conterá muitos parâmetros relacionados à autenticação. No ADAL Node, o objeto AuthenticationContext tem um número limitado de parâmetros de configuração com os quais você pode criar uma instância, enquanto os parâmetros restantes permanecem livres no código (por exemplo, clientSecret):

var adal = require('adal-node');

var authority = "https://login.microsoftonline.com/YOUR_TENANT_ID"
var validateAuthority = true,
var cache = null;

var authenticationContext = new adal.AuthenticationContext(authority, validateAuthority, cache);
  • authority: URL que identifica uma autoridade de token
  • validateAuthority: um recurso que impede que o código solicite tokens de uma autoridade potencialmente mal-intencionada
  • cache: define o cache de token usado por esta instância AuthenticationContext. Se este parâmetro não estiver definido, será usado um padrão no cache de memória

O MSAL Node, por outro lado, usa um objeto de configuração do tipo Configuration. Ele contém as propriedades a seguir:

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

const msalConfig = {
    auth: {
        clientId: "YOUR_CLIENT_ID",
        authority: "https://login.microsoftonline.com/YOUR_TENANT_ID",
        clientSecret: "YOUR_CLIENT_SECRET",
        knownAuthorities: [],
    },
    cache: {
        // your implementation of caching
    },
    system: {
        loggerOptions: { /** logging related options */ }
    }
}


const cca = new msal.ConfidentialClientApplication(msalConfig);

Como uma diferença notável, a MSAL não tem um sinalizador para desabilitar a validação de autoridade e as autoridades são sempre validadas por padrão. A MSAL compara a autoridade solicitada com uma lista de autoridades conhecidas pela Microsoft ou uma lista de autoridades que você especificou em sua configuração. Para mais informações confira: Opções de configuração

Alternar para a API da MSAL

A maioria dos métodos públicos no ADAL Node tem equivalentes no MSAL Node:

ADAL MSAL Observações
acquireToken acquireTokenSilent Renomeado e agora espera um objeto account
acquireTokenWithAuthorizationCode acquireTokenByCode
acquireTokenWithClientCredentials acquireTokenByClientCredential
acquireTokenWithRefreshToken acquireTokenByRefreshToken Útil para migrar tokens de atualização válidos
acquireTokenWithDeviceCode acquireTokenByDeviceCode Agora abstrai a aquisição de código do usuário (veja abaixo)
acquireTokenWithUsernamePassword acquireTokenByUsernamePassword

No entanto, alguns métodos no ADAL Node foram preteridos, enquanto o MSAL Node oferece novos métodos:

ADAL MSAL Observações
acquireUserCode N/D Mesclado com acquireTokeByDeviceCode (veja acima)
N/D acquireTokenOnBehalfOf Um novo método que abstrai o fluxo OBO
acquireTokenWithClientCertificate N/D Não é mais necessário, pois os certificados agora são atribuídos durante a inicialização (confira opções de configuração)
N/D getAuthCodeUrl Um novo método que abstrai a construção da URL do ponto de extremidade de autorização

Use escopos em vez de recursos

Uma diferença importante entre pontos de extremidade v1.0 vs. v2.0 tem a ver com o modo como os recursos são acessados. No ADAL Node, você deverá primeiro registrar uma permissão no portal de registro do aplicativo e, depois, solicitar um token de acesso para um recurso (como o Microsoft Graph), conforme mostrado abaixo:

authenticationContext.acquireTokenWithAuthorizationCode(
    req.query.code,
    redirectUri,
    resource, // e.g. 'https://graph.microsoft.com'
    clientId,
    clientSecret,
    function (err, response) {
        // do something with the authentication response
    }
);

O nó MSAL só dá suporte ao ponto de extremidade v2.0. O ponto de extremidade 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:

const tokenRequest = {
    code: req.query.code,
    scopes: ["https://graph.microsoft.com/User.Read"],
    redirectUri: REDIRECT_URI,
};

pca.acquireTokenByCode(tokenRequest).then((response) => {
    // do something with the authentication response
}).catch((error) => {
    console.log(error);
});

Uma vantagem do modelo centrado no escopo é a capacidade de usar escopos dinâmicos. Ao criar aplicativos com a v1.0, era preciso registrar o conjunto completo de permissões (chamado escopos estáticos) exigidas pelo aplicativo para que o usuário consentisse durante o momento do 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

No ADAL Node, os retornos de chamada são usados para todas as operações após a autenticação ser bem-sucedida e uma resposta for obtida:

var context = new AuthenticationContext(authorityUrl, validateAuthority);

context.acquireTokenWithClientCredentials(resource, clientId, clientSecret, function(err, response) {
    if (err) {
        console.log(err);
    } else {
        // do something with the authentication response
    }
});

No MSAL Node, as promessas são usadas:

    const cca = new msal.ConfidentialClientApplication(msalConfig);

    cca.acquireTokenByClientCredential(tokenRequest).then((response) => {
        // do something with the authentication response
    }).catch((error) => {
        console.log(error);
    });

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

    try {
        const authResponse = await cca.acquireTokenByCode(tokenRequest);
    } catch (error) {
        console.log(error);
    }

Habilitar o registro em log

No ADAL Node, você configura o registro em log separadamente em qualquer lugar no código:

var adal = require('adal-node');

//PII or OII logging disabled. Default Logger does not capture any PII or OII.
adal.logging.setLoggingOptions({
  log: function (level, message, error) {
    console.log(message);

    if (error) {
        console.log(error);
    }
  },
  level: logging.LOGGING_LEVEL.VERBOSE, // provide the logging level
  loggingWithPII: false  // Determine if you want to log personal identification information. The default value is false.
});

No MSAL Node, o registro em log faz parte das opções de configuração e é criado com a inicialização da instância do MSAL Node:

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

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 cca = new msal.ConfidentialClientApplication(msalConfig);

Ativar cache de token

No ADAL Node, você tinha a opção de importar um cache de token na memória. O cache de token é usado como um parâmetro ao inicializar um objeto AuthenticationContext:

var MemoryCache = require('adal-node/lib/memory-cache');

var cache = new MemoryCache();
var authorityURI = "https://login.microsoftonline.com/common";

var context = new AuthenticationContext(authorityURI, true, cache);

O MSAL Node usa um cache de token na memória por padrão. Você não precisa importá-lo explicitamente, o cache de token de memória é exposto como parte das classes ConfidentialClientApplication e PublicClientApplication.

const msalTokenCache = publicClientApplication.getTokenCache();

O mais importante é que seu cache de tokens anterior com o ADAL Node não será transferível para o MSAL Node, já que os esquemas de cache são incompatíveis. No entanto, você pode usar os tokens de atualização válidos que seu aplicativo obteve anteriormente com o ADAL Node no MSAL Node. Confira a seção sobre tokens de atualização para saber mais.

Você também pode gravar o cache em disco fornecendo seu próprio plug-in de cache. O plugin de cache deve implementar a interface ICachePlugin. Assim como no registro em log, o cache faz parte das opções de configuração e é criado com a inicialização da instância do MSAL Node:

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

const msalConfig = {
    auth: {
        // authentication related parameters
    },
    cache: {
        cachePlugin // your implementation of cache plugin
    },
    system: {
        // logging related options
    }
}

const msalInstance = new ConfidentialClientApplication(msalConfig);

Um plug-in de cache de exemplo pode ser implementado como mostrado abaixo:

const fs = require('fs');

// Call back APIs which automatically write and read into a .json file - example implementation
const beforeCacheAccess = async (cacheContext) => {
    cacheContext.tokenCache.deserialize(await fs.readFile(cachePath, "utf-8"));
};

const afterCacheAccess = async (cacheContext) => {
    if(cacheContext.cacheHasChanged) {
        await fs.writeFile(cachePath, cacheContext.tokenCache.serialize());
    }
};

// Cache Plugin
const cachePlugin = {
    beforeCacheAccess,
    afterCacheAccess
};

Se você estiver desenvolvendo aplicativos cliente públicos, como aplicativos da área de trabalho, as Extensões de Autenticação da Microsoft para Node oferecerão mecanismos seguros para que os aplicativos cliente executem a serialização e persistência do cache de tokens de plataforma cruzada. As plataformas com suporte são Windows, Mac e Linux.

Observação

As Extensões de Autenticação da Microsoft para Nodenão são recomendadas para aplicativos Web, pois podem resultar em problemas de escala e desempenho. Em vez disso, os aplicativos Web são recomendados para manter o cache na sessão.

Remover lógica em torno de tokens de atualização

No ADAL Node, os RT (tokens de atualização) foram expostos, permitindo que você desenvolva soluções em torno do uso desses tokens, armazenando-os em cache e usando o método acquireTokenWithRefreshToken. Cenários típicos em que os RTs são especialmente relevantes:

  • Serviços de execução prolongada que realizam ações, incluindo a atualização de painéis em nome dos usuários quando eles não estão mais conectados.
  • Cenários WebFarm para permitir que o cliente traga o RT para o serviço Web (o cache é feito no lado do cliente, cookie criptografado, e não do servidor).

O MSAL Node, junto com outros MSALs, não expõe tokens de atualização por motivos de segurança. Em vez disso, o MSAL lida com a atualização de tokens para você. Dessa forma, você não precisa mais do log de build para isso. No entanto, você pode usar os tokens de atualização adquiridos anteriormente (e ainda válidos) do cache do ADAL Node para obter um novo conjunto de tokens com o MSAL Node. Para fazer isso, o MSAL Node oferece acquireTokenByRefreshToken, que é equivalente ao método acquireTokenWithRefreshToken do ADAL Node:

var msal = require('@azure/msal-node');

const config = {
    auth: {
        clientId: "ENTER_CLIENT_ID",
        authority: "https://login.microsoftonline.com/ENTER_TENANT_ID",
        clientSecret: "ENTER_CLIENT_SECRET"
    }
};

const cca = new msal.ConfidentialClientApplication(config);

const refreshTokenRequest = {
    refreshToken: "", // your previous refresh token here
    scopes: ["https://graph.microsoft.com/.default"],
    forceCache: true,
};

cca.acquireTokenByRefreshToken(refreshTokenRequest).then((response) => {
    console.log(response);
}).catch((error) => {
    console.log(error);
});

Para obter mais informações, consulte o exemplo de migração do ADAL Node para o MSAL Node.

Observação

Recomendamos a destruição do cache de token mais antigo do ADAL Node depois de utilizar os tokens de atualização ainda válidos para obter um novo conjunto de tokens usando o método acquireTokenByRefreshToken do MSAL Node, conforme mostrado acima.

Lidar com erros e exceções

Ao usar o MSAL Node, o tipo mais comum de erro que você pode enfrentar é o erro interaction_required. Ele geralmente é resolvido com a inicialização de um prompt de aquisição de token interativo. Por exemplo, ao usar acquireTokenSilent, se não houver tokens de atualização armazenados em cache, o MSAL Node não poderá adquirir um token de acesso silenciosamente. Da mesma forma, 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). Nesses casos, o tratamento do erro interaction_required pelo disparo de acquireTokenByCode solicitará ao usuário a MFA, permitindo que ele a utilize.

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 o método acquireTokenByCode.

Executar o aplicativo

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

npm start

Exemplo: como adquirir tokens com ADAL Node versus MSAL Node

O snippet a seguir demonstra um aplicativo Web cliente confidencial na estrutura Express.js. Ele executa uma entrada quando um usuário acessa a rota de autenticação /auth, adquire um token de acesso para o Microsoft Graph por meio da rota /redirect e exibe o conteúdo do token específico.

Usando ADAL Node Usando MSAL Node
// Import dependencies
var express = require('express');
var crypto = require('crypto');
var adal = require('adal-node');

// Authentication parameters
var clientId = 'Enter_the_Application_Id_Here';
var clientSecret = 'Enter_the_Client_Secret_Here';
var tenant = 'Enter_the_Tenant_Info_Here';
var authorityUrl = 'https://login.microsoftonline.com/' + tenant;
var redirectUri = 'http://localhost:3000/redirect';
var resource = 'https://graph.microsoft.com';

// Configure logging
adal.Logging.setLoggingOptions({
    log: function (level, message, error) {
        console.log(message);
    },
    level: adal.Logging.LOGGING_LEVEL.VERBOSE,
    loggingWithPII: false
});

// Auth code request URL template
var templateAuthzUrl = 'https://login.microsoftonline.com/'
    + tenant + '/oauth2/authorize?response_type=code&client_id='
    + clientId + '&redirect_uri=' + redirectUri
    + '&state=<state>&resource=' + resource;

// Initialize express
var app = express();

// State variable persists throughout the app lifetime
app.locals.state = "";

app.get('/auth', function(req, res) {

    // Create a random string to use against XSRF
    crypto.randomBytes(48, function(ex, buf) {
        app.locals.state = buf.toString('base64')
            .replace(/\//g, '_')
            .replace(/\+/g, '-');

        // Construct auth code request URL
        var authorizationUrl = templateAuthzUrl
            .replace('<state>', app.locals.state);

        res.redirect(authorizationUrl);
    });
});

app.get('/redirect', function(req, res) {
    // Compare state parameter against XSRF
    if (app.locals.state !== req.query.state) {
        res.send('error: state does not match');
    }

    // Initialize an AuthenticationContext object
    var authenticationContext =
        new adal.AuthenticationContext(authorityUrl);

    // Exchange auth code for tokens
    authenticationContext.acquireTokenWithAuthorizationCode(
        req.query.code,
        redirectUri,
        resource,
        clientId,
        clientSecret,
        function(err, response) {
            res.send(response);
        }
    );
});

app.listen(3000, function() {
    console.log(`listening on port 3000!`);
});
// Import dependencies
const express = require("express");
const msal = require('@azure/msal-node');

// Authentication parameters
const config = {
    auth: {
        clientId: "Enter_the_Application_Id_Here",
        authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here",
        clientSecret: "Enter_the_Client_Secret_Here"
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: msal.LogLevel.Verbose,
        }
    }
};

const REDIRECT_URI = "http://localhost:3000/redirect";

// Initialize MSAL Node object using authentication parameters
const cca = new msal.ConfidentialClientApplication(config);

// Initialize express
const app = express();

app.get('/auth', (req, res) => {

    // Construct a request object for auth code
    const authCodeUrlParameters = {
        scopes: ["user.read"],
        redirectUri: REDIRECT_URI,
    };

    // Request auth code, then redirect
    cca.getAuthCodeUrl(authCodeUrlParameters)
        .then((response) => {
            res.redirect(response);
        }).catch((error) => res.send(error));
});

app.get('/redirect', (req, res) => {

    // Use the auth code in redirect request to construct
    // a token request object
    const tokenRequest = {
        code: req.query.code,
        scopes: ["user.read"],
        redirectUri: REDIRECT_URI,
    };

    // Exchange the auth code for tokens
    cca.acquireTokenByCode(tokenRequest)
        .then((response) => {
            res.send(response);
        }).catch((error) => res.status(500).send(error));
});

app.listen(3000, () =>
    console.log(`listening on port 3000!`));

Próximas etapas