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

Обзор

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

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

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

В следующих разделах рассматриваются особенности каждого шага.

Создайте вызов

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

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Чтобы задание выполнило свою задачу, вы должны:

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

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

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

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

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

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

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

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

После создания publicKeyCredentialRequestOptions отправьте его клиенту.

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

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

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

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} 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 . Он представляет собой ответ поставщика ключа доступа на инструкцию клиента создать то, что необходимо для попытки аутентификации с помощью ключа доступа на RP. Это содержит:

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

На сервере сделайте следующее:

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

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

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

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

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

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  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.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      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 });
  }
});

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

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

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

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