Миграция приложения Node.js из ADAL в MSAL

Microsoft Authentication Library for Node (MSAL Node) — это рекомендуемый пакет SDK для включения проверки подлинности и авторизации приложений, зарегистрированных на платформе удостоверений Майкрософт. В этой статье рассматриваются важные действия, которые необходимо выполнить для переноса приложений из библиотеки проверки подлинности Active Directory для узла (ADAL Node) в MSAL Node.

Предварительные требования

Обновление параметров регистрации приложения

При работе с узлом ADAL, скорее всего, вы использовали конечную точку Azure AD версии 1.0. Приложения, перенесенные из ADAL в MSAL, должны переключиться на конечную точку Azure AD версии 2.0.

Установка и импорт MSAL

  1. установите пакет MSAL для Node через npm:
  npm install @azure/msal-node
  1. После этого импортируйте MSAL Node в код:
  const msal = require('@azure/msal-node');
  1. Наконец, удалите пакет ADAL Node и все ссылки на него в коде:
  npm uninstall adal-node

Инициализация MSAL

В ADAL Node вы инициализируете AuthenticationContext объект, который затем предоставляет методы, которые можно использовать в разных потоках проверки подлинности (например, acquireTokenWithAuthorizationCode для веб-приложений). При инициализации единственным обязательным параметром является URI органа:

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

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

В MSAL Node вместо этого у вас есть два варианта: если вы создаете мобильное или настольное приложение, вы создаете экземпляр объекта PublicClientApplication. Конструктор ожидает объект конфигурации , содержащий clientId параметр по крайней мере. MSAL по умолчанию использует URI полномочий https://login.microsoftonline.com/common, если вы его не указали.

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

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

Примечание.

Если вы используете полномочия https://login.microsoftonline.com/common в версии 2.0, пользователи смогут выполнять вход с помощью любой организации Microsoft Entra или личной учетной записи Microsoft (MSA). В MSAL Node, если вы хотите ограничить вход для любой учетной записи Microsoft Entra (то же поведение, что и в ADAL Node), используйте вместо этого https://login.microsoftonline.com/organizations.

С другой стороны, если вы создаете веб-приложение или управляющее приложение, вы создаёте экземпляр ConfidentialClientApplication объекта. В таких приложениях также необходимо указать учетные данные клиента, например секрет клиента или сертификат:

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

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

Оба PublicClientApplication и ConfidentialClientApplication, в отличие от ADAL AuthenticationContext, привязаны к идентификатору клиента. Это означает, что при наличии различных идентификаторов клиентов, которые вы хотите использовать в приложении, необходимо создать еще по одному экземпляру MSAL для каждого из них. Дополнительные сведения: инициализация узла MSAL

Настройка MSAL

При создании приложений на платформе Microsoft Identity приложение будет содержать много параметров, связанных с проверкой подлинности. В ADAL Node AuthenticationContext объект имеет ограниченное количество параметров конфигурации, с которыми можно его инициализировать, а остальные параметры остаются свободными в коде (например, 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-адрес, определяющий орган выдачи токена
  • validateAuthority: функция, которая запрещает код запрашивать маркеры от потенциально вредоносного центра.
  • cache: задает кэш токенов, используемый данным экземпляром AuthenticationContext. Если этот параметр не задан, то используется значение по умолчанию в кэше памяти.

С другой стороны MSAL Node использует объект конфигурации типа Configuration. Он содержит следующие свойства:

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

В качестве заметной разницы, MSAL не имеет флага для отключения проверки авторитета, и авторитеты всегда проверяются по умолчанию. MSAL сравнивает запрошенный центр авторизации со списком центров авторизации, известных корпорации Майкрософт, или со списком центров авторизации, указанных в конфигурации. Дополнительные сведения см. в разделе "Параметры конфигурации"

Переход на API MSAL

Для большинства открытых методов в ADAL Node имеются эквиваленты в MSAL Node:

АДАЛ МСАЛ (Майкрософтская библиотека аутентификации) Примечания.
acquireToken acquireTokenSilent Переименовано и теперь ожидает объект учетной записи
acquireTokenWithAuthorizationCode acquireTokenByCode
acquireTokenWithClientCredentials acquireTokenByClientCredential
acquireTokenWithRefreshToken acquireTokenByRefreshToken Полезно для переноса допустимых маркеров обновления
acquireTokenWithDeviceCode acquireTokenByDeviceCode Теперь абстрагирует получение кода пользователя (см. ниже)
acquireTokenWithUsernamePassword acquireTokenByUsernamePassword

Однако некоторые методы в ADAL Node являются устаревшими, тогда как в MSAL Node доступны новые методы:

АДАЛ МСАЛ (Майкрософтская библиотека аутентификации) Примечания.
acquireUserCode Н/П Объединенный с acquireTokeByDeviceCode (см. выше)
Н/П acquireTokenOnBehalfOf Новый метод, который абстрагирует поток OBO
acquireTokenWithClientCertificate Н/П Больше не требуется по мере назначения сертификатов во время инициализации (см. параметры конфигурации)
Н/П getAuthCodeUrl Новый метод, который упрощает построение URL-адреса конечной точки авторизации

Использование областей вместо ресурсов

Важное различие между конечными точками версии 1.0 и 2.0 заключается в том, как осуществляется доступ к ресурсам. В узле ADAL сначала необходимо зарегистрировать разрешение на портале регистрации приложений, а затем запросить маркер доступа для ресурса (например, Microsoft Graph), как показано ниже:

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

MSAL Node поддерживает только конечную точку версии 2.0. Конечная точка версии 2.0 использует модель, ориентированную на область , для доступа к ресурсам. Таким образом, при запросе токена доступа для ресурса также необходимо указать область для этого ресурса:

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

Одним из преимуществ модели, ориентированной на область, является возможность использования динамических областей. При создании приложений с помощью версии 1.0 необходимо зарегистрировать полный набор разрешений (называемых статическими областями), необходимый приложению для предоставления пользователю согласия во время входа. В версии 2.0 можно использовать параметр области для запроса разрешений в то время, когда они нужны (следовательно, динамические области). Это позволяет пользователю предоставлять добавочное согласие на области. Таким образом, в начале работы вы можете позволить пользователю просто войти в приложение без запроса прав доступа, если они пока не нужны. Когда вам понадобятся возможности чтения календаря пользователя, вы можете запросить область доступа к календарю в методах получения маркера и получить согласие пользователя. См. подробнее: ресурсы и области применений

Использование обещаний вместо обратных вызовов

В ADAL Node обратные вызовы используются для любой операции после успешной проверки подлинности и получения ответа:

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

В MSAL Node вместо них используются обещания:

    const cca = new msal.ConfidentialClientApplication(msalConfig);

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

Вы также можете использовать синтаксис async/await , который поставляется с ES8:

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

Включение ведения журналов

В ADAL Node необходимо отдельно настраивать ведение журнала в любом месте кода:

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

В MSAL Node ведение журнала относится к параметрам конфигурации и устанавливается при инициализации экземпляра 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);

Включить кэширование токенов

В ADAL Node имеется возможность импортировать кэш токенов в памяти. Кэш токенов используется в качестве параметра при инициализации 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);

MSAL Node по умолчанию использует кэш токенов в памяти. Не нужно явно импортировать его; кэш токенов в памяти предоставляется как часть классов ConfidentialClientApplication и PublicClientApplication.

const msalTokenCache = publicClientApplication.getTokenCache();

Важно отметить, что предыдущий кэш маркеров с помощью узла ADAL не будет передаваться в MSAL Node, так как схемы кэша несовместимы. Однако в MSAL Node вы можете использовать действительные маркеры обновления, полученные ранее с помощью ADAL Node. Дополнительные сведения см. в разделе о маркерах обновления .

Вы также можете записать кэш на диск, предоставив собственный плагин кэша. Плагин кэша должен реализовать интерфейс ICachePlugin. Как и ведение журнала, кэширование относится к параметрам конфигурации и устанавливается при инициализации экземпляра 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);

Пример плагина кэша можно реализовать следующим образом:

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

Если вы разрабатываете общедоступные клиентские приложения , такие как классические приложения, расширения проверки подлинности Майкрософт для Node предлагают безопасные механизмы для клиентских приложений для выполнения сериализации и сохраняемости межплатформенного кэша маркеров. Поддерживаемые платформы Windows, Mac и Linux.

Примечание.

Расширения проверки подлинности Майкрософт для Node.jsне рекомендуется использовать для веб-приложений, так как это может привести к проблемам с масштабированием и производительностью. Вместо этого для сохранения кэша в сеансе рекомендуется использовать веб-приложения.

Удалить логику, связанную с токенами обновления

В ADAL Node токены обновления (RT) были предоставлены, позволяя разрабатывать решения, использующие эти токены путем их кэширования и использования метода acquireTokenWithRefreshToken. Типичные сценарии, когда RT особенно важны:

  • Службы длительного действия, которые выполняют действия, включая обновление панелей мониторинга, от имени пользователей, когда они больше не подключены.
  • В сценариях WebFarm предоставляется возможность клиенту передавать RT в веб-службу (кэширование осуществляется на стороне клиента с использованием зашифрованного cookie, а не на сервере).

MSAL Node, а также другие MSALs, не предоставляют маркеры обновления по соображениям безопасности. Вместо этого MSAL автоматически обрабатывает обновление токенов для вас. Поэтому вам больше не нужно создавать эту логику. Однако вы можете использовать ранее приобретенные (и по-прежнему допустимые) токены обновления из кэша библиотеки ADAL Node, чтобы получить новый набор токенов с библиотеки MSAL Node. Для этого MSAL Node предлагает acquireTokenByRefreshToken, что эквивалентно методу ADAL Node acquireTokenWithRefreshToken:

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

Дополнительные сведения см. в примерах узлов MSAL.

Примечание.

Рекомендуем уничтожить старый кэш маркеров ADAL после использования еще действительных маркеров обновления, чтобы получить новый набор маркеров с помощью метода MSAL Node acquireTokenByRefreshToken, как показано выше.

Обработка ошибок и исключений

При использовании MSAL Node наиболее распространенный тип ошибки, с которой вы можете столкнуться, это ошибка interaction_required. Эта ошибка часто устраняется путем инициирования запроса на получение интерактивных маркеров. Например, при использовании acquireTokenSilent при отсутствии кэшированных маркеров обновления MSAL Node не сможет получить токен доступа в тихом режиме. Аналогичным образом, веб-API, к которому вы пытаетесь получить доступ, может иметь политику условного доступа, требуя от пользователя выполнения многофакторной проверки подлинности (MFA). В таких случаях обработка interaction_required ошибок с последующим запуском acquireTokenByCode запросит у пользователя выполнение MFA, что дает возможность его выполнить.

Еще одна распространенная ошибка, consent_required, возникает, когда пользователь не дает согласия на разрешения, необходимые для получения токена доступа к защищенному ресурсу. Как и в случае interaction_required, решение ошибки consent_required часто заключается в инициировании интерактивного запроса на получение токена с помощью метода acquireTokenByCode.

Выполнить приложение

После внесения изменений запустите приложение и протестируйте сценарий проверки подлинности:

npm start

Пример: получение токенов с использованием ADAL Node и MSAL Node

В приведенном ниже фрагменте кода демонстрируется конфиденциальное клиентское веб-приложение на платформе Express.js. Он выполняет вход, когда пользователь попадает в маршрут проверки подлинности /auth, получает маркер доступа для Microsoft Graph через маршрут /redirect, а затем отображает содержимое указанного токена.

Использование узла ADAL Использование узла MSAL
// 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!`));

Следующие шаги