Поделиться через


Лучшие практики работы с учетными данными

В этой статье приведены рекомендации по управлению маркерами доступа пользователей в пакетах SDK служб коммуникации Azure. Следуйте этим рекомендациям, чтобы оптимизировать ресурсы, используемые вашим приложением, и уменьшить количество обращений к API для удостоверения личности в службах связи Azure.

Учетные данные токена связи

Учетные данные токена связи (учетные данные) — это механизм аутентификации, который оборачивает токены доступа пользователя. Он используется для проверки подлинности пользователей в службах коммуникации, таких как чат или звонки. Кроме того, он предоставляет встроенные функции обновления токенов для удобства разработчика.

Выбор времени существования сеанса

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

Настройка времени истечения действия пользовательского токена

При запросе нового токена мы рекомендуем использовать токены с коротким сроком действия для разовых сообщений в чате или сеансов звонков с ограниченным временем действия. Мы рекомендуем использовать более длинные маркеры времени существования для агентов, использующих приложение в течение длительного периода времени. Срок действия токена по умолчанию составляет 24 часа. Можно настроить время окончания срока действия, указав значение от часа до 24 часов для дополнительного параметра следующим образом:

const tokenOptions = { tokenExpiresInMinutes: 60 };
const user = { communicationUserId: userId };
const scopes = ["chat"];
let communicationIdentityToken = await identityClient.getToken(user, scopes, tokenOptions);

Статический маркер

Для краткосрочных клиентов инициализировать учетные данные со статическим токеном. Этот подход подходит для таких сценариев, как отправка одноуровневых сообщений чата или сеансов звонков с ограниченным временем.

let communicationIdentityToken = await identityClient.getToken({ communicationUserId: userId }, ["chat", "voip"]);
const tokenCredential = new AzureCommunicationTokenCredential(communicationIdentityToken.token);

Функция обратного вызова

Для долгосрочных клиентов инициализируйте учетные данные с помощью функции обратного вызова, которая обеспечивает непрерывное состояние проверки подлинности во время обмена данными. Этот подход подходит, например, для длительных сеансов звонков.

const tokenCredential = new AzureCommunicationTokenCredential({
            tokenRefresher: async (abortSignal) => fetchTokenFromMyServerForUser(abortSignal, "<user_name>")
        });

Обновление токенов

Чтобы правильно реализовать обратный вызов средства обновления маркеров, код должен вернуть строку с допустимым веб-маркером JSON (JWT). Возвращаемый маркер всегда должен быть допустимым, а дата окончания срока действия должна быть задана в будущем. Некоторые платформы, такие как JavaScript и .NET, предлагают способ отменить операцию обновления и передать AbortSignal или CancellationToken вашей функции. Мы рекомендуем принять эти объекты, использовать их или передать их дальше.

Пример 1. Обновление токена для коммуникационного пользователя

Предположим, что у нас есть приложение Node.js, созданное в Express с /getToken конечной точкой, позволяющей получить новый действительный маркер для пользователя, указанного по имени.

app.post('/getToken', async (req, res) => {
    // Custom logic to determine the communication user id
    let userId = await getCommunicationUserIdFromDb(req.body.username);
    // Get a fresh token
    const identityClient = new CommunicationIdentityClient("<COMMUNICATION_SERVICES_CONNECTION_STRING>");
    let communicationIdentityToken = await identityClient.getToken({ communicationUserId: userId }, ["chat", "voip"]);
    res.json({ communicationIdentityToken: communicationIdentityToken.token });
});

Затем необходимо реализовать обратный вызов механизма обновления токенов в клиентском приложении, правильно используя AbortSignal и возвращая строку JWT с распакованным содержанием.

const fetchTokenFromMyServerForUser = async function (abortSignal, username) {
    const response = await fetch(`${HOST_URI}/getToken`,
        {
            method: "POST",
            body: JSON.stringify({ username: username }),
            signal: abortSignal,
            headers: { 'Content-Type': 'application/json' }
        });

    if (response.ok) {
        const data = await response.json();
        return data.communicationIdentityToken;
    }
};

Пример 2. Обновление токена для пользователя Teams

Предположим, что у нас есть приложение Node.js, созданное на основе Express с /getTokenForTeamsUser конечной точкой, позволяющей обменять токен доступа Microsoft Entra пользователя Teams на новый токен доступа для средств связи с совпадающим сроком действия.

app.post('/getTokenForTeamsUser', async (req, res) => {
    const identityClient = new CommunicationIdentityClient("<COMMUNICATION_SERVICES_CONNECTION_STRING>");
    let communicationIdentityToken = await identityClient.getTokenForTeamsUser(req.body.teamsToken, '<AAD_CLIENT_ID>', '<TEAMS_USER_OBJECT_ID>');
    res.json({ communicationIdentityToken: communicationIdentityToken.token });
});

Далее необходимо реализовать функцию обратного вызова для обновления маркеров в клиентском приложении, которая отвечает за следующее:

  1. Обновите токен доступа Microsoft Entra для пользователя Teams.
  2. Обменяйте маркер доступа Microsoft Entra пользователя Teams на токен доступа к удостоверению коммуникации.
const fetchTokenFromMyServerForUser = async function (abortSignal, username) {
    // 1. Refresh the Azure AD access token of the Teams User
    let teamsTokenResponse = await refreshAadToken(abortSignal, username);

    // 2. Exchange the Azure AD access token of the Teams User for a Communication Identity access token
    const response = await fetch(`${HOST_URI}/getTokenForTeamsUser`,
        {
            method: "POST",
            body: JSON.stringify({ teamsToken: teamsTokenResponse.accessToken }),
            signal: abortSignal,
            headers: { 'Content-Type': 'application/json' }
        });

    if (response.ok) {
        const data = await response.json();
        return data.communicationIdentityToken;
    }
}

В этом примере мы используем библиотеку проверки подлинности Майкрософт (MSAL) для обновления маркера доступа Microsoft Entra. Следуя руководству по получению маркера Microsoft Entra для вызова API, сначала мы пытаемся получить маркер без взаимодействия пользователя. Если это невозможно, мы активируем один из интерактивных потоков.

const refreshAadToken = async function (abortSignal, username) {
    if (abortSignal.aborted === true) throw new Error("Operation canceled");

    // MSAL.js v2 exposes several account APIs; the logic to determine which account to use is the responsibility of the developer. 
    // In this case, we'll use an account from the cache.    
    let account = (await publicClientApplication.getTokenCache().getAllAccounts()).find(u => u.username === username);

    const renewRequest = {
        scopes: [
            "https://auth.msft.communication.azure.com/Teams.ManageCalls",
            "https://auth.msft.communication.azure.com/Teams.ManageChats"
        ],
        account: account,
        forceRefresh: forceRefresh
    };
    let tokenResponse = null;
    // Try to get the token silently without the user's interaction    
    await publicClientApplication.acquireTokenSilent(renewRequest).then(renewResponse => {
        tokenResponse = renewResponse;
    }).catch(async (error) => {
        // In case of an InteractionRequired error, send the same request in an interactive call
        if (error instanceof InteractionRequiredAuthError) {
            // You can choose the popup or redirect experience (`acquireTokenPopup` or `acquireTokenRedirect` respectively)
            publicClientApplication.acquireTokenPopup(renewRequest).then(function (renewInteractiveResponse) {
                tokenResponse = renewInteractiveResponse;
            }).catch(function (interactiveError) {
                console.log(interactiveError);
            });
        }
    });
    return tokenResponse;
}

Предоставление начального маркера

Для дальнейшей оптимизации кода можно извлечь токен при запуске приложения и передать его в Credential напрямую. Предоставление начального маркера пропускает первый вызов функции обратного вызова обновления при сохранении всех последующих вызовов.

const tokenCredential = new AzureCommunicationTokenCredential({
            tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_id>"),
            token: "<initial_token>"
        });

Упреждающее обновление токена

Используйте упреждающее обновление, чтобы устранить любую возможную задержку во время получения маркера по запросу. Упреждающее обновление обновляет маркер в фоновом режиме в конце его существования. Когда срок действия маркера истекает, 10 минут до окончания его срока действия учетные данные начинают пытаться получить маркер. Он активирует обратный вызов средства обновления с увеличением частоты до тех пор, пока он не будет успешно получен маркера с достаточной сроком действия.

const tokenCredential = new AzureCommunicationTokenCredential({
            tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_id>"),
            refreshProactively: true
        });

Если вы хотите отменить запланированные задачи обновления, удалите объект Credential.

Проактивное обновление токена для пользователя Teams

Чтобы свести к минимуму количество циклов в API удостоверений Коммуникации Azure, убедитесь, что маркер Microsoft Entra, который вы передаете для обмена , имеет достаточно времени действия (> 10 минут). В случае, если MSAL возвращает кэшированный маркер с более коротким сроком действия, у вас есть следующие параметры для обхода кэша:

  1. Принудительное обновление токена
  2. Увеличьте окно обновления токена MSAL до более чем 10 минут.

Вариант 1. Запуск потока получения токена с AuthenticationParameters.forceRefresh установленным в true.

// Extend the `refreshAadToken` function 
const refreshAadToken = async function (abortSignal, username) {

    // ... existing refresh logic

    // Make sure the token has at least 10-minute lifetime and if not, force-renew it
    if (tokenResponse.expiresOn < (Date.now() + (10 * 60 * 1000))) {
        const renewRequest = {
            scopes: [
                "https://auth.msft.communication.azure.com/Teams.ManageCalls",
                "https://auth.msft.communication.azure.com/Teams.ManageChats"
            ],
            account: account,
            forceRefresh: true // Force-refresh the token
        };        
        
        await publicClientApplication.acquireTokenSilent(renewRequest).then(renewResponse => {
            tokenResponse = renewResponse;
        });
    }
}

Вариант 2. Инициализация контекста аутентификации MSAL путем создания пользовательского экземпляра PublicClientApplication с SystemOptions.tokenRenewalOffsetSeconds.

const publicClientApplication = new PublicClientApplication({
    system: {
        tokenRenewalOffsetSeconds: 900 // 15 minutes (by default 5 minutes)
    });

Отмена обновления

Чтобы клиенты связи могли отменить текущие задачи обновления, необходимо передать объект отмены в колбэк обновления. Этот шаблон применяется только к JavaScript и .NET.

var controller = new AbortController();

var joinChatBtn = document.querySelector('.joinChat');
var leaveChatBtn = document.querySelector('.leaveChat');

joinChatBtn.addEventListener('click', function () {
    // Wrong:
    const tokenCredentialWrong = new AzureCommunicationTokenCredential({
        tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_name>")
    });

    // Correct: Pass abortSignal through the arrow function
    const tokenCredential = new AzureCommunicationTokenCredential({
        tokenRefresher: async (abortSignal) => fetchTokenFromMyServerForUser(abortSignal, "<user_name>")
    });

    // ChatClient is now able to abort token refresh tasks
    const chatClient = new ChatClient("<endpoint-url>", tokenCredential);

    // Pass the abortSignal to the chat client through options
    const createChatThreadResult = await chatClient.createChatThread(
        { topic: "Hello, World!" },
        {
            // ...
            abortSignal: controller.signal
        }
    );

    // ...
});

leaveChatBtn.addEventListener('click', function() {
    controller.abort();
    console.log('Leaving chat...');
});

Если вы хотите отменить последующие задачи обновления, удалите объект Credential.

Очистка ресурсов

Так как объект Credential можно передать нескольким экземплярам клиентов чата или звонков, пакет SDK не делает предположений о его времени существования и оставляет ответственность за его удаление разработчику. Приложениям служб связи принадлежит обязанность удалить экземпляр учетных данных, когда они больше не нужны. Удаление учетных данных также отменяет запланированные действия обновления при включении упреждающего обновления.

Вызовите функцию .dispose().

const tokenCredential = new AzureCommunicationTokenCredential("<token>");
// Use the credential for Calling or Chat
const chatClient = new ChatClient("<endpoint-url>", tokenCredential);
// ...
tokenCredential.dispose()

Обработка выхода

В зависимости от вашего сценария может потребоваться выйти из учетной записи пользователя в одной или нескольких службах.


Дальнейшие действия

В этой статье описано, как:

  • Правильная инициализация и удаление объекта Credential
  • Реализация обратного вызова механизма обновления токенов
  • Оптимизируйте логику обновления токенов