Аутентификация с помощью ключа доступа на стороне сервера

Обзор

Ниже приведены основные этапы аутентификации с помощью ключа доступа.

Процесс аутентификации с помощью ключа доступа

  • Определите запрос и другие параметры, необходимые для аутентификации с помощью ключа доступа. Отправьте их клиенту, чтобы передать в вызов аутентификации с помощью ключа доступа (navigator.credentials.get в веб-версии). После того как пользователь подтвердит аутентификацию с помощью ключа доступа, вызов аутентификации будет разрешен и вернет учетные данные (PublicKeyCredential). Учетные данные содержат утверждение аутентификации.
  • Проверьте утверждение аутентификации.
  • Если утверждение аутентификации действительно, выполните аутентификацию пользователя.

Ниже приведены подробные инструкции для каждого шага.

Создать соревнование

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

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Чтобы соревнование было эффективным, необходимо:

  1. Убедитесь, что один и тот же запрос не используется более одного раза. Генерируйте новый запрос при каждой попытке входа. Удаляйте запрос после каждой попытки входа, независимо от того, была ли она успешной. Также можно отменить проверку по истечении определенного времени. Никогда не принимайте один и тот же вызов в ответе более одного раза.
  2. Убедитесь, что запрос криптографически защищен. Проверка должна быть такой, чтобы ее было практически невозможно угадать. Чтобы создать криптографически защищенный запрос на стороне сервера, лучше всего использовать надежную серверную библиотеку FIDO. Если вы создаете собственные проверки, используйте встроенные криптографические функции, доступные в вашем наборе технологий, или найдите библиотеки, предназначенные для криптографических целей. Примеры: iso-crypto в Node.js или secrets в Python. Согласно спецификации, запрос должен быть длиной не менее 16 байт, чтобы считаться безопасным.

После создания запроса сохраните его в сеансе пользователя, чтобы проверить позже.

Создать параметры запроса учетных данных

Создайте параметры запроса учетных данных в виде объекта publicKeyCredentialRequestOptions.

Для этого используйте серверную библиотеку FIDO. Обычно в ней есть служебная функция, которая позволяет создавать такие объекты. Например, SimpleWebAuthn предлагает generateAuthenticationOptions.

publicKeyCredentialRequestOptions должен содержать всю информацию, необходимую для аутентификации с помощью ключа доступа. Передайте эту информацию функции в серверной библиотеке FIDO, которая отвечает за создание объекта publicKeyCredentialRequestOptions.

Некоторые поля publicKeyCredentialRequestOptions могут быть константами. Другие должны динамически определяться на сервере:

  • rpId – идентификатор проверяющей стороны, с которым должны быть связаны учетные данные, например example.com. Аутентификация будет успешной, только если указанный вами идентификатор RP совпадает с идентификатором RP, связанным с учетными данными. Чтобы заполнить идентификатор проверяющей стороны, используйте то же значение, которое вы указали в publicKeyCredentialCreationOptions при регистрации учетных данных.
  • challenge – фрагмент данных, который поставщик ключа доступа подпишет, чтобы подтвердить, что у пользователя есть ключ доступа на момент запроса аутентификации. Подробнее о том, как создать челлендж…
  • allowCredentials – массив допустимых учетных данных для этой аутентификации. Передайте пустой массив, чтобы пользователь мог выбрать доступный ключ доступа из списка, показанного браузером. Подробную информацию можно найти в разделах Получение запроса от сервера проверяющей стороны и Подробные сведения о видимых учетных данных.
  • userVerification. Указывает, является ли подтверждение личности пользователя с помощью блокировки экрана устройства обязательным, предпочтительным или нежелательным. Ознакомьтесь с разделом Получение запроса от сервера проверяющей стороны.
  • timeout – время (в миллисекундах), в течение которого пользователь должен пройти аутентификацию. Оно должно быть достаточно большим, но меньше срока действия challenge. Рекомендуемое значение по умолчанию – 5 минут, но вы можете увеличить его до 10 минут, что по-прежнему находится в пределах рекомендуемого диапазона. Длительное время ожидания имеет смысл, если вы ожидаете, что пользователи будут использовать гибридный рабочий процесс, который обычно занимает немного больше времени. Если время ожидания истечет, будет выдана ошибка NotAllowedError.

Создав publicKeyCredentialRequestOptions, отправьте его клиенту.

publicKeyCredentialCreationOptions, отправленные сервером
Варианты, отправленные сервером. challenge декодирование происходит на стороне клиента.

Пример кода: создание параметров запроса учетных данных

В наших примерах используется библиотека SimpleWebAuthn. В этом примере мы передаем создание параметров запроса учетных данных функции generateAuthenticationOptions.

import {
  generateAuthenticationOptions,
} from '@simplewebauthn/server';

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

Проверка и вход пользователя

Если метод navigator.credentials.get успешно выполняется на стороне клиента, он возвращает объект PublicKeyCredential.

Объект PublicKeyCredential, отправленный сервером
navigator.credentials.get возвращает PublicKeyCredential.

response – это AuthenticatorAssertionResponse. Это ответ поставщика ключей доступа на инструкцию клиента создать то, что нужно для попытки аутентификации с помощью ключа доступа на проверяющей стороне. Содержание раздела:

Отправьте объект PublicKeyCredential на сервер.

На сервере выполните следующие действия:

Схема базы данных
Рекомендуемая схема базы данных. Подробнее об этом читайте в статье Регистрация ключа доступа на стороне сервера.
  • Соберите информацию, которая понадобится для проверки утверждения и аутентификации пользователя:
    • Получите ожидаемый запрос, сохраненный в сеансе при создании параметров аутентификации.
    • Получите ожидаемый источник и идентификатор проверяющей стороны.
    • Найдите в базе данных, кто этот пользователь. В случае с обнаруживаемыми учетными данными вы не знаете, кто отправляет запрос на аутентификацию. Узнать это можно двумя способами:
      • Вариант 1. Используйте свойство response.userHandle в объекте PublicKeyCredential. В таблице Пользователи найдите значение passkey_user_id, соответствующее userHandle.
      • Вариант 2. Используйте учетные данные id, указанные в объекте PublicKeyCredential. В таблице Учетные данные открытого ключа найдите учетные данные id, которые соответствуют учетным данным id в объекте PublicKeyCredential. Затем найдите соответствующего пользователя, используя внешний ключ passkey_user_id в таблице Users.
    • Найдите в базе данных информацию об учетных данных открытого ключа, которая соответствует полученному утверждению аутентификации. Для этого в таблице Учетные данные открытого ключа найдите учетные данные id, которые соответствуют учетным данным id в объекте PublicKeyCredential.
  • Проверьте утверждение аутентификации. Передайте этот шаг проверки серверной библиотеке FIDO, которая обычно предлагает для этой цели служебную функцию. Например, SimpleWebAuthn предлагает verifyAuthenticationResponse. Подробнее о том, что происходит за кулисами, читайте в разделе Приложение. Проверка ответа аутентификации.

  • Удалите запрос независимо от того, успешно ли выполнена проверка, чтобы предотвратить атаки повторного воспроизведения.

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

Пример кода: проверка и вход пользователя

В наших примерах используется библиотека SimpleWebAuthn. Здесь мы передаем проверку ответа аутентификации функции verifyAuthenticationResponse.

import {
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Prepare credential information for verification
    const credential = {
      id: cred.id,
      publicKey: isoBase64URL.toBuffer(cred.publicKey),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      credential,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('User verification failed.');
    }

    // Kill the challenge for this session.
    delete req.session.challenge;

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

Приложение. Проверка ответа аутентификации

Проверка ответа на аутентификацию состоит из следующих этапов:

  • Убедитесь, что идентификатор проверяющей стороны соответствует вашему сайту.
  • Убедитесь, что источник запроса совпадает с источником входа на сайт. Для приложений Android ознакомьтесь с разделом Проверка источника.
  • Убедитесь, что устройство смогло предоставить заданную вами проверку.
  • Убедитесь, что при аутентификации пользователь выполнил требования, которые вы установили как проверяющая сторона. Если вы требуете подтверждения пользователя, убедитесь, что флаг uv (пользователь подтвержден) в authenticatorData имеет значение true. Убедитесь, что флаг up (присутствие пользователя) в authenticatorData имеет значение true, поскольку для ключей доступа всегда требуется присутствие пользователя.
  • Проверьте подпись. Чтобы проверить подпись, вам потребуется:
    • Подпись, которая представляет собой подписанный запрос: response.signature.
    • Открытый ключ для проверки подписи.
    • Исходные подписанные данные. Это данные, подпись которых нужно проверить.
    • криптографический алгоритм, который был использован при создании подписи;

Чтобы узнать больше об этих шагах, ознакомьтесь с исходным кодом SimpleWebAuthn для verifyAuthenticationResponse или изучите полный список проверок в спецификации.