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

Обзор

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

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

  • Определите параметры для создания ключа доступа. Отправьте их клиенту, чтобы вы могли передать их в вызов создания ключа доступа: вызов 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 : для регистрации учетных данных запрос не имеет значения, если вы не используете аттестацию, более продвинутый метод проверки личности поставщика ключей доступа и данных, которые он передает. Однако даже если вы не используете аттестацию, запрос по-прежнему является обязательным полем. В этом случае для простоты вы можете установить для этого задания один 0 . Инструкции по созданию безопасного запроса на аутентификацию доступны в разделе Аутентификация с использованием пароля на стороне сервера .

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

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 = 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 , который представляет собой ответ поставщика ключа доступа на инструкцию клиента по созданию ключа доступа. Он содержит информацию о новых учетных данных, которые вам понадобятся в качестве RP для последующей аутентификации пользователя. Дополнительные сведения об 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 : используйте его как внешний ключ для установления связи с таблицей «Пользователи» .
    • backed_up : резервная копия ключа доступа сохраняется , если он синхронизирован поставщиком ключа доступа. Сохранение состояния резервной копии полезно, если вы хотите рассмотреть возможность удаления паролей в будущем для пользователей, у которых есть ключи доступа backed_up . Вы можете проверить, создана ли резервная копия ключа доступа, проверив флаги в authenticatorData или используя функцию серверной библиотеки FIDO, которая обычно доступна для облегчения доступа к этой информации. Сохранение соответствия критериям резервного копирования может быть полезно для ответа на запросы потенциальных пользователей.
    • name : опционально отображаемое имя учетных данных, позволяющее пользователям присваивать учетным данным собственные имена.
    • transports : Массив транспортов . Хранение транспортов полезно для аутентификации пользователя. Когда транспорты доступны, браузер может вести себя соответствующим образом и отображать пользовательский интерфейс, соответствующий транспорту, который поставщик ключей доступа использует для связи с клиентами — в частности, для случаев повторной аутентификации, когда allowCredentials не пуст.

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

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

В наших примерах мы используем библиотеку 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 { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // 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 , который не имеет значения, если вы не используете аттестацию.
    • authenticatorData — это данные, видимые поставщиком ключа доступа. В качестве RP чтение authenticatorData дает вам доступ к данным, видимым поставщиком ключей доступа и возвращаемым во время запроса create .

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

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

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

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

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

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

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

Дальше

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