Logo Craft Homelab Docs Контакты Telegram
Passkey в веб-приложении: серверная проверка WebAuthn Трендовые github проекты в нашем телеграм канале. Подпишись →
30 июля 2026 г.

Серверная логика для надёжного входа по passkey

Passkey строятся поверх WebAuthn и позволяют привязать вход к криптографической паре ключей. Закрытый ключ остаётся у аутентификатора: в менеджере паролей, платформенном хранилище или аппаратном ключе. Сервер сохраняет идентификатор учётных данных и открытый ключ, а при входе проверяет подпись. Надёжность схемы определяется главным образом тем, насколько строго сервер валидирует контекст операции и бинарные данные от браузера.

Ниже — последовательность, которая подходит для собственного backend-сервиса. Для промышленной реализации полезно использовать зрелую библиотеку WebAuthn, однако протокол и границы проверок должны быть понятны команде и без неё.

Что хранить рядом с пользователем

Для каждой зарегистрированной учётной записи понадобятся как минимум:

  • credentialId — уникальный бинарный идентификатор учётных данных;
  • открытый ключ и алгоритм подписи;
  • идентификатор пользователя, с которым связаны учётные данные;
  • последнее значение счётчика подписей, если аутентификатор его выдаёт;
  • при необходимости AAGUID и имя устройства для интерфейса управления ключами.

Идентификатор нельзя принимать повторно: при регистрации сервер проверяет его уникальность. Хранить бинарные значения следует в кодировке, которая не меняет байты, например base64url или BLOB. Данные challenge также стоит держать отдельно: с TTL, признаком использования и привязкой к конкретной операции или сессии.

Регистрация: параметры и результат браузера

Перед navigator.credentials.create() backend формирует PublicKeyCredentialCreationOptions. В rp.id указывается ожидаемый relying party ID, обычно hostname приложения. Поле user.id должно быть стабильным байтовым идентификатором пользователя, а не отображаемым именем.

В pubKeyCredParams имеет смысл предложить распространённые алгоритмы: ES256 (-7), RS256 (-257) и EdDSA (-8). ES256 с кривой P-256 имеет наиболее широкую поддержку. RS256 важен для совместимости со старыми учётными данными Windows. EdDSA использует Ed25519 и часто доступен на ключах безопасности.

Passkey — это обнаруживаемые учётные данные. Для них выбирают residentKey: "required" и userVerification: "required"; устаревший requireResidentKey можно оставить для обратной совместимости. Аттестация обычно не требуется, поэтому подходит attestation: "none". Список excludeCredentials содержит уже привязанные credentialId и предотвращает повторную регистрацию на том же аутентификаторе.

После успешного вызова браузер вернёт PublicKeyCredential, содержащий AuthenticatorAttestationResponse. На сервер необходимо передать объект аттестации, clientDataJSON и идентификатор учётных данных. Эти поля содержат критичные для проверки бинарные данные; преобразование в JSON должно быть аккуратным и обратимым.

Какие проверки нужны при создании ключа

Сначала проверяется clientDataJSON. В нём должны совпасть:

  • тип операции webauthn.create;
  • challenge, выпущенный сервером;
  • origin из списка доверенных источников;
  • ожидаемое значение crossOrigin, если операция выполняется во фрейме.

Далее разбираются данные аутентификатора. Первые 32 байта содержат SHA-256 от ID relying party — их сверяют с ожидаемым значением. Флаги указывают на присутствие пользователя, пройденную верификацию пользователя, наличие attested credentials и расширений. При обязательной пользовательской верификации сервер должен увидеть соответствующий флаг. При регистрации также ожидается флаг attested credentials.

Оставшаяся часть содержит AAGUID, длину и значение credentialId, а затем открытый ключ COSE в CBOR. Ключ проверяют в соответствии с заявленным алгоритмом. Для ES256 нужна кривая P-256; для RS256 практичным ограничением будет модуль 2048 бит и открытая экспонента 65537; для EdDSA ожидается Ed25519. Библиотека верификации обязана безопасно отвергать некорректные ключи без аварийного завершения процесса.

Если сервис запросил расширения, сервер сверяет их содержимое с ожидаемым. Неожиданные extension data лучше отклонять либо отдельно логировать и разбирать по политике продукта. После успешной валидации сохраняются учётные данные, а challenge инвалидируется. Его можно инвалидировать после каждой попытки или после успеха, но политика должна быть единообразной.

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

Для аутентификации backend выпускает новый криптографически случайный challenge и передаёт его в navigator.credentials.get(). При входе с конкретным ключом allowCredentials ограничивает допустимые идентификаторы. При passkey без предварительного ввода пользователя этот список может отсутствовать: аутентификатор сам предложит обнаруживаемые учётные данные. Параметр userVerification: "required" сохраняет требование к биометрии или PIN.

Ответ содержит authenticatorData, clientDataJSON, signature, rawId и иногда userHandle. Сервер снова сверяет тип webauthn.get, challenge и origin. Затем он находит сохранённый ключ по rawId и разбирает данные аутентификатора.

Подпись проверяется над точной конкатенацией двух значений:

authenticatorData || SHA-256(clientDataJSON)

Проверка выполняется сохранённым открытым ключом и соответствующим алгоритмом. Ошибка подписи, другой RP ID hash, отсутствие требуемых флагов присутствия пользователя или user verification означают отказ во входе. Challenge после попытки должен стать недействительным, чтобы его нельзя было воспроизвести повторно.

Счётчики подписей и инциденты

В authenticatorData есть 32-битный счётчик подписей. Многие современные синхронизируемые passkey возвращают ноль: это допустимое значение, означающее отсутствие поддержки механизма. Аппаратные токены чаще увеличивают счётчик после каждой операции.

Если сохранённый и новый счётчики ненулевые, новое значение должно быть больше прежнего. Несовпадение служит полезным сигналом для журналирования и ручного разбора: оно может указывать на клонирование ключа, но не доказывает компрометацию. Жёсткая блокировка пользователя в такой ситуации создаёт риск отказа в доступе законному владельцу. Более безопасная операционная реакция — зафиксировать событие, запросить повторную проверку при повышенном риске и дать пользователю управлять своими ключами.

Удобный вход без отдельной кнопки

Conditional UI добавляет passkey в выпадающий список браузера рядом с полем email. Поле получает autocomplete="webauthn", после загрузки страницы вызывается navigator.credentials.get() с mediation: "conditional". Обещание может оставаться активным, пока пользователь не выберет ключ, поэтому его не стоит делать зависимостью обычной загрузки страницы.

Для такого сценария особенно важен срок действия challenge. Пользователь способен оставить вкладку открытой надолго. Сервису нужно отменять запрос после истечения TTL или перевыпускать его по контролируемой схеме.

Контрольный список backend-команды

  1. Генерируйте новый challenge криптографическим генератором и ограничивайте его срок жизни.
  2. Валидируйте type, challenge, origin и RP ID hash для каждого ответа браузера.
  3. Требуйте флаги присутствия пользователя и user verification там, где это задано политикой.
  4. Проверяйте формат, кривую или параметры каждого открытого ключа до сохранения.
  5. Сверяйте подпись над исходными бинарными байтами, не над их текстовым представлением.
  6. Учитывайте счётчики как сигнал мониторинга и ведите аудит событий.
  7. Давайте пользователю посмотреть, переименовать и удалить привязанные passkey.

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