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

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

Серверный

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

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

  1. Использовать стандартный идентификатор проверяющей стороны. В качестве постоянной строки используется стандартный идентификатор проверяющей стороны.
  2. Сгенерируйте запрос. Создайте криптографически защищенный случайный буфер с высокой энтропией, надежно сохраните его в сеансе пользователя и закодируйте в формате Base64URL.
  3. Сопоставление обнаруживаемых учетных данных. Укажите пустой массив [] для allowCredentials. Это запрашивает видимые учетные данные, то есть пользователю не нужно сначала вводить свое имя пользователя; поставщик ключа доступа предоставит доступные учетные записи.
  4. Уровень подтверждения пользователя. Установите значение userVerification: "preferred" (или "required", если это явно предписано корпоративными правилами).
    • Запрошенный уровень ограничений userVerification ДОЛЖЕН сохраняться в записи сеанса сервера в конечной точке параметров, а не передаваться обратно от клиента через строки запроса. Это позволяет конечной точке проверки безопасно применять строгие ограничения на сопоставление без риска манипуляций со стороны клиента.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

Конечная точка подтверждения

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

  1. Проверка запроса сеанса. Обеспечивает строгое соответствие между ответом клиента и ожидаемым запросом, сохраненным в сеансе.
  2. Принудительное применение настроек UV:
    • Разрешите аутентификаторы без проверки пользователя (например, с отключенной блокировкой экрана), если в сеансе запрашивается expectedUserVerification"preferred". Для этого передайте requireUserVerification: false в библиотеку проверки на стороне сервера. Если запрашивается "required", строго требовать биометрические данные или PIN-код.
  3. Ошибка сервера 404. Если идентификатор учетных данных, возвращенный клиентом, не найден в базе данных, возвращается ошибка HTTP 404, чтобы клиент мог активировать Signal API.

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

Аннотация формы HTML

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

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

Процесс с явной кнопкой

Запускайте аутентификацию с помощью ключа доступа, когда пользователь нажимает кнопку "Войти с помощью ключа доступа". Прежде чем показывать запрос ключа доступа, прервите все текущие вызовы условного получения для автозаполнения форм.

Условный процесс медиации (автозаполнение формы)

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

  1. Обнаружение функции. Вызовите PublicKeyCredential.getClientCapabilities() при загрузке страницы и пропустите вход с помощью ключа доступа, если conditionalGet недоступен.
  2. Декодирование параметров. Декодируйте полученный объект JSON с учетными данными с помощью PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Вызов условного метода GET. Вызовите navigator.credentials.get() с помощью mediation: "conditional" и передайте сигнал AbortController. Это регистрирует автозаполнение без показа диалогового окна ключа доступа.
  4. Разделение исключений Try/Catch. Оберните вызов navigator.credentials.get в блок try/catch:
    • NotAllowedError – пользователь отменил запрос на вход с помощью ключа доступа или время ожидания истекло.
    • AbortError: запрос на аутентификацию был отменен программно.
  5. Call Signal API. Оберните вызов fetch() для проверки сервера в блок try/catch:
    • Покажите сообщение об ошибке, чтобы пользователь понял, что пошло не так.
    • Вызывайте signalUnknownCredential(), ТОЛЬКО когда сервер явно возвращает статус HTTP 404 (учетные данные не найдены) и пользователь не аутентифицирован.
    • Параметр credentialId, передаваемый в signalUnknownCredential(), ДОЛЖЕН быть строкой идентификатора учетных данных, закодированной по стандарту Base64URL (например, encoded.id), а НЕ необработанным объектом ArrayBuffer credential.rawId.
  6. Закодируйте ответ. Закодируйте учетные данные AuthenticatorAssertionResponse с помощью .toJSON(), прежде чем отправлять их на сервер для проверки.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

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

Резервный вариант обнаружения функции ключей доступа

Всегда устанавливайте webauthn-polyfills и импортируйте его в контекст. Учтите, что если поддерживается PublicKeyCredential, то поддерживается и PublicKeyCredential.getClientCapabilities.

import 'webauthn-polyfills';

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

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

  • Резервный вариант. Доступен через if (PublicKeyCredential.signalUnknownCredential). Если нет, то фоновая синхронизация проверки пропускается без ошибок браузера.

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

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

import 'webauthn-polyfills';