Регистрация пароля на стороне сервера

Обзор

Ниже приведен краткий обзор основных этапов регистрации ключа доступа:

Процесс регистрации ключа доступа

  • Определите параметры для создания ключа доступа. Отправьте их клиенту, чтобы затем передать их в вызов для создания ключа доступа: вызов API WebAuthn navigator.credentials.create в веб-версии и credentialManager.createCredential в Android. После того, как пользователь подтвердит создание ключа доступа, вызов для создания ключа доступа будет выполнен и вернет учётные данные PublicKeyCredential .
  • Проверьте учетные данные и сохраните их на сервере.

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

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

Первый шаг, который необходимо сделать на сервере, — создать объект PublicKeyCredentialCreationOptions .

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

PublicKeyCredentialCreationOptions должен включать всё необходимое для создания ключа доступа: информацию о пользователе, о проверяющей стороне (RP) и конфигурацию свойств создаваемых учётных данных. После определения всех этих параметров передайте их при необходимости функции в серверной библиотеке FIDO, которая отвечает за создание объекта PublicKeyCredentialCreationOptions .

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

  • rpId : Чтобы заполнить идентификатор RP на сервере, используйте серверные функции или переменные, которые предоставляют вам имя хоста вашего веб-приложения, например example.com .
  • user.name и user.displayName : для заполнения этих полей используйте данные сеанса вошедшего в систему пользователя (или данные новой учётной записи, если пользователь создаёт ключ доступа при регистрации). user.name — это обычно адрес электронной почты, уникальный для RP. user.displayName — удобное для пользователя имя. Обратите внимание, что displayName поддерживается не на всех платформах.
  • user.id — случайная уникальная строка, генерируемая при создании учётной записи. Она должна быть постоянной, в отличие от имени пользователя, которое можно редактировать. Идентификатор пользователя идентифицирует учётную запись, но не должен содержать никакой персональной информации (PII) . Вероятно, в вашей системе уже есть идентификатор пользователя, но при необходимости создайте его специально для паролей, чтобы защитить его от любых персональных данных.
  • excludeCredentials : список идентификаторов существующих учётных данных для предотвращения дублирования ключа доступа от поставщика ключа доступа. Чтобы заполнить это поле, найдите в базе данных существующие учётные данные этого пользователя. Подробнее см. в разделе «Запретить создание нового ключа доступа, если он уже существует» .
  • challenge : для регистрации учётных данных запрос не имеет значения, если вы не используете аттестацию — более продвинутый метод проверки личности поставщика ключа доступа и передаваемых им данных. Однако даже без аттестации запрос остаётся обязательным. Инструкции по созданию безопасного запроса для аутентификации см. в разделе «Аутентификация с помощью ключа доступа на стороне сервера» .

Кодирование и декодирование

PublicKeyCredentialCreationOptions, отправленные сервером
PublicKeyCredentialCreationOptions отправляются сервером. challenge , user.id и excludeCredentials.credentials должны быть закодированы на стороне сервера в base64URL , чтобы PublicKeyCredentialCreationOptions можно было доставить по протоколу HTTPS.

PublicKeyCredentialCreationOptions включают поля типа ArrayBuffer , поэтому они не поддерживаются методом JSON.stringify() . Это означает, что в настоящее время для доставки PublicKeyCredentialCreationOptions по HTTPS некоторые поля необходимо вручную кодировать на сервере с помощью base64URL , а затем декодировать на клиенте.

  • На сервере кодирование и декодирование обычно выполняется серверной библиотекой FIDO.
  • На клиенте кодирование и декодирование сейчас необходимо выполнять вручную. В будущем это станет проще: будет доступен метод преобразования параметров из JSON в PublicKeyCredentialCreationOptions . Проверьте статус реализации в Chrome.

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

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

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

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // 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 {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = await generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

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

Сохранить открытый ключ

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

Успешное разрешение navigator.credentials.create на клиенте означает, что ключ доступа был успешно создан. Возвращается объект PublicKeyCredential .

Объект PublicKeyCredential содержит объект AuthenticatorAttestationResponse , представляющий ответ поставщика ключа доступа на инструкцию клиента о его создании. Он содержит информацию о новых учётных данных, которые понадобятся вам как проверяющей стороне для последующей аутентификации пользователя. Подробнее об AuthenticatorAttestationResponse . в Приложении: AuthenticatorAttestationResponse .

Отправьте объект PublicKeyCredential на сервер. После получения проверьте его.

Передайте этот этап проверки вашей серверной библиотеке FIDO. Обычно она предоставляет для этого вспомогательную функцию. SimpleWebAuthn предлагает, например, verifyRegistrationResponse . Подробнее о том, что происходит «под капотом», читайте в Приложении: проверка ответа на запрос регистрации .

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

Используйте отдельную таблицу для учётных данных открытого ключа, связанных с ключами доступа. У пользователя может быть только один пароль, но несколько ключей доступа, например, один, синхронизированный через Apple iCloud Keychain, и один, синхронизированный через Google Password Manager.

Вот пример схемы, которую можно использовать для хранения учетных данных:

Схема базы данных для ключей доступа

  • Таблица пользователей :
    • user_id : основной идентификатор пользователя. Случайный, уникальный и постоянный идентификатор пользователя. Используйте его в качестве первичного ключа для таблицы «Пользователи» .
    • username . Имя пользователя, определяемое пользователем и потенциально доступное для редактирования.
    • passkey_user_id : идентификатор пользователя, не содержащий персональных данных, указанный в user.id в параметрах регистрации . При последующей попытке аутентификации пользователя аутентификатор предоставит passkey_user_id в ответе на запрос аутентификации в userHandle . Мы рекомендуем не использовать passkey_user_id в качестве первичного ключа. Первичные ключи, как правило, фактически становятся персональными данными в системах, поскольку они широко используются.
  • Таблица учетных данных открытого ключа :
    • id : идентификатор учётных данных. Используйте его в качестве первичного ключа для таблицы учётных данных открытого ключа .
    • public_key : Открытый ключ учетных данных.
    • passkey_user_id : используйте это как внешний ключ для установления связи с таблицей Users .
    • backed_up : Резервная копия ключа доступа создается, если он синхронизирован поставщиком ключей доступа. Сохранение состояния резервной копии полезно, если вы планируете в будущем отказаться от паролей пользователей, хранящих ключи доступа, backed_up . Вы можете проверить наличие резервной копии ключа доступа, проверив флаг BE в authenticatorData или воспользовавшись функцией серверной библиотеки FIDO, которая обычно доступна для легкого доступа к этой информации. Сохранение информации о наличии резервной копии может быть полезно для ответа на потенциальные запросы пользователей.
    • name : При необходимости отображаемое имя для учетных данных, позволяющее пользователям давать учетным данным собственные имена.
    • transports : Массив транспортов . Хранение транспортов полезно для пользовательского опыта аутентификации. При наличии транспортов браузер может вести себя соответствующим образом и отображать пользовательский интерфейс, соответствующий транспорту, который поставщик ключей доступа использует для взаимодействия с клиентами, — в частности, для случаев повторной аутентификации, когда allowCredentials непустой.

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

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

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

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


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // 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 {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

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

    const {
      aaguid,
      credentialPublicKey,
      credentialID,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '000000-0000-0000-0000-00000000' ?
        req.useragent?.platform : aaguids[aaguid].name;

    const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
    const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

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

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

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

Приложение: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse содержит два важных объекта:

  • response.clientDataJSON — это JSON-версия клиентских данных , которые в вебе отображаются в браузере. Они содержат источник RP, запрос и androidPackageName , если клиент — приложение Android. В качестве RP чтение clientDataJSON даёт доступ к информации, которую браузер видел во время запроса create .
  • response.attestationObject содержит два фрагмента информации:
    • attestationStatement , который не имеет значения, если вы не используете attestation.
    • authenticatorData — это данные, которые видит поставщик ключа доступа. Как RP, чтение authenticatorData даёт вам доступ к данным, которые видит поставщик ключа доступа и которые возвращаются при запросе create .

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

  • Собственно учетные данные открытого ключа и уникальный идентификатор учетных данных для него.
  • Идентификатор RP, связанный с учетными данными.
  • Флаги, описывающие статус пользователя при создании ключа доступа: присутствовал ли пользователь на самом деле и был ли пользователь успешно проверен (см. подробное описание userVerification ).
  • AAGUID — это идентификатор поставщика ключа доступа, например, Google Password Manager. На основе AAGUID можно определить поставщика ключа доступа и отобразить его имя на странице управления ключами доступа . (См. раздел Определение поставщика ключа доступа с помощью AAGUID ).

Несмотря на то, что authenticatorData вложен в attestationObject , содержащаяся в нём информация необходима для реализации вашего ключа доступа независимо от того, используете ли вы аттестацию. authenticatorData закодирован и содержит поля, закодированные в двоичном формате. Обычно разбором и декодированием занимается ваша серверная библиотека. Если вы не используете серверную библиотеку, рассмотрите возможность использования getAuthenticatorData() на стороне клиента, чтобы сэкономить время на разбор и декодирование на стороне сервера.

Приложение: проверка ответа на регистрацию

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

  • Убедитесь, что идентификатор RP соответствует вашему сайту.
  • Убедитесь, что источник запроса соответствует ожидаемому источнику для вашего сайта (URL основного сайта, приложение Android).
  • Если вам требуется проверка пользователя, убедитесь, что флаг проверки пользователя authenticatorData.uv имеет true .
  • Обычно ожидается, что флаг присутствия пользователя authenticatorData.up будет иметь true , но если учетные данные созданы условно , ожидается, что он будет иметь значение false .
  • Проверьте, смог ли клиент выполнить поставленную вами задачу. Если вы не используете аттестацию, эта проверка не важна. Тем не менее, её реализация — это наилучшая практика: она гарантирует готовность вашего кода, если вы решите использовать аттестацию в будущем.
  • Убедитесь, что идентификатор учетной записи еще не зарегистрирован ни для одного пользователя.
  • Убедитесь, что алгоритм, используемый поставщиком ключа доступа для создания учётных данных, — это алгоритм, указанный вами (в каждом поле alg файла publicKeyCredentialCreationOptions.pubKeyCredParams , который обычно определяется в вашей серверной библиотеке и не виден вам). Это гарантирует, что пользователи смогут регистрироваться только с помощью разрешённых вами алгоритмов.

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

Следующий

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