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

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

Требования к базе данных

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

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

Серверный

Генерирование вариантов

Создайте конечную точку, которая генерирует параметры создания WebAuthn. Используйте проверенные библиотеки, соответствующие стандартам категорий, а не создавайте криптографические решения самостоятельно.

  1. Использовать стандартный идентификатор проверяющей стороны. В качестве постоянной строки используется стандартный идентификатор проверяющей стороны.
  2. Создайте безопасный запрос. Сгенерируйте на сервере криптографически безопасный случайный буфер с высокой энтропией, надежно сохраните его в сеансе пользователя и закодируйте в формате Base64URL для передачи параметров.
  3. Избегайте дублирования ключей доступа. Сопоставьте существующие идентификаторы предварительно зарегистрированных учетных данных пользователя с массивом параметров excludeCredentials. Это предотвращает регистрацию дублирующихся учетных данных в одном аккаунте поставщика ключей доступа.
  4. Принудительное использование обнаруживаемых учетных данных. Задайте для параметров requireResidentKey: true и residentKey: "required" в authenticatorSelection значение, которое запрашивает обнаруживаемые учетные данные. Это необходимо для входа с помощью обнаруживаемых учетных данных.
  5. Настройте проверку пользователей. Укажите userVerification: "preferred" или userVerification: "required". Во многих случаях, когда требуется соблюдение нормативных требований (например, в сфере финансов или здравоохранения), 'required' требует подтверждения личности пользователя при создании.
  6. Определите область действия прикрепления
    • Продвижение. Если вы предлагаете создать ключ доступа сразу после стандартного входа с паролем или регистрации, задайте для authenticatorAttachment: "platform" принудительное использование аутентификатора платформы и пропуск запросов внешнего электронного ключа.
    • Процесс управления. Если вызов выполняется из специальной панели настроек или безопасности, где помимо аутентификатора платформы поддерживаются внешние электронные ключи, полностью опустите свойство authenticatorAttachment.
    • Совет. Чтобы условно обрабатывать оба потока с помощью одной конечной точки, примите флаг запроса promotion: boolean.
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

Проверка

  1. Проверка запроса. Безопасная проверка запроса на соответствие ожидаемому запросу, связанному с сеансом.
  2. Проверка присутствия пользователя
    • Убедитесь, что флаг User Present (UP), возвращенный в обработанных данных аутентификатора, имеет значение true. Это подтверждает физическое присутствие пользователя в момент создания.
  3. Упрощенная проверка для предпочтительных доменов
    • Если в параметрах создания указано userVerification: "preferred", то вызов проверки на стороне сервера ДОЛЖЕН быть настроен с помощью requireUserVerification: false. В противном случае аутентификаторы, которые регистрируются без подтверждения пользователя (например, если отключена блокировка экрана), будут вызывать ложные сбои проверки сервера.

Клиентская логика

  1. Ограничение доступа к интерфейсу при загрузке страницы
    • При загрузке страницы вызовите PublicKeyCredential.getClientCapabilities() и отключите кнопку "Создать ключ доступа", если conditionalGet или passkeyPlatformAuthenticator недоступны.
  2. Вызов создания и сериализация. Декодируйте параметры сервера с помощью PublicKeyCredential.parseCreationOptionsFromJSON() и передайте полученную конфигурацию в navigator.credentials.create().
    • Вызовите credential.toJSON(), чтобы закодировать AuthenticatorAttestationResponse в действительный сериализуемый объект JSON, прежде чем запрашивать конечную точку подтверждения.
  3. Обработка исключений WebAuthn
    • InvalidStateError: соответствующий ключ доступа уже существует (совпадение по excludeCredentials).
    • NotAllowedError: пользователь закрыл диалоговое окно аутентификации с помощью ключа доступа или время ожидания истекло.
    • AbortError. Операция прервана.
    • SecurityError – ошибки, связанные с защищенными источниками (HTTPS) или несоответствием идентификатора проверяющей стороны (проблемы с конфигурацией).
  4. Разделение Try/Catch для Signal API:
    • Оберните вызов проверки сервера fetch() в блок try/catch. Вызовите signalUnknownCredential(), если не удалось получить подтверждение от сервера (любой статус response.ok === false или сетевые ошибки).
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

Резервные стратегии

Резервный вариант синхронизации Signal API

WebAuthn Signal API (webauthn-signals) – это прогрессивная оптимизация, которая используется для синхронизации менеджеров паролей с состоянием учетных данных на сервере.

  • Резервный вариант. Если браузер не поддерживает PublicKeyCredential.signalUnknownCredential, вызов ДОЛЖЕН быть безопасно обойден с помощью шлюза обнаружения функций (if (PublicKeyCredential.signalUnknownCredential)), а проверка на стороне сервера просто регистрирует сбой, не запуская обновления менеджера.

Простая резервная сериализация JSON

Всегда устанавливайте webauthn-polyfills и импортируйте его в контекст. Если поддерживается PublicKeyCredential, то поддерживаются и PublicKeyCredential.parseCreationOptionsFromJSON, и PublicKeyCredential.prototype.toJSON.

import 'webauthn-polyfills';