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

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

Необходимые компоненты

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

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

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

  1. установите пакет УЗЛА MSAL через 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 authenticationContex = 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 или личной учетной записи Майкрософт (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 Node

Настройка 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:

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

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

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

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

Важное различие между конечными точками версии 1.0 и 2.0 заключается в том, как осуществляется доступ к ресурсам. В ADAL Node сначала необходимо зарегистрировать разрешение на портале регистрации приложений, а затем запросить токен доступа для ресурса (например, 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не рекомендуется использовать для веб-приложений, так как это может привести к проблемам масштабирования и производительности. Вместо этого для сохранения кэша в сеансе рекомендуется использовать веб-приложения.

Удаление логики, касающейся токенов обновления

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

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

Узел MSAL, а также другие msALs, не предоставляет маркеры обновления по соображениям безопасности. Вместо этого MSAL обрабатывает токены обновления. Поэтому вам больше не нужно создавать эту логику. Однако вы можете использовать полученные ранее (и сохраняющие действительность) маркеры обновления из кэша ADAL Node, чтобы получить новый набор маркеров для MSAL Node. Для этого MSAL Node предлагает acquireTokenByRefreshToken — эквивалент метода acquireTokenWithRefreshToken в 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);
});

Дополнительные сведения см. в примере миграции с ADAL Node на MSAL Node.

Примечание.

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

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

При использовании MSAL Node наиболее распространенным типом ошибки может стать interaction_required. Эта ошибка часто устраняется путем инициирования запроса на получение интерактивных маркеров. Например, при использовании acquireTokenSilentпри отсутствии кэшированных маркеров обновления узел MSAL не сможет автоматически получить маркер доступа. Аналогичным образом, веб-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!`));

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