WebAuthn UserHandle Null: Как исправить ассоциацию PassKey
Узнайте, почему WebAuthn UserHandle равен null при использовании allowCredentials и как надежно связывать пользователей с PassKey на всех устройствах, включая Samsung. Полное руководство по реализации с примерами кода.
Почему UserHandle равен null в WebAuthn/PassKey Attestation при использовании allowCredentials, и как можно надежно связывать пользователей с их PassKeys?
Я реализую аутентификацию PassKey и столкнулся с проблемой, когда UserHandle постоянно равен null, когда я указываю ‘allowCredentials’ в вызове credentials.get(). Когда я убираю allowCredentials, UserHandle заполняется правильно.
Текущая реализация:
var getAssertionOptions = {
timeout: 50000,
challenge: stringToArrayBuffer(serverChallenge.Token),
allowCredentials : allowCredentials
};
В моем коде верификации:
if (
assertion == null ||
assertion.Id == null ||
assertion.AuthenticatorData == null ||
assertion.ClientDataJSON == null ||
assertion.Signature == null
) {
// assertion.UserHandle равен null для Samsung телефона
return FAIL_STATUS;
}
Основные вопросы:
-
Является ли UserHandle постоянно равным null для Samsung телефонов, даже когда requireResidentKey установлен в true при создании учетных данных?
-
Как можно надежно связывать пользователей сайта с их PassKeys в моей базе данных, если UserHandle равен null? Следует ли использовать ID Assertion вместо этого?
-
Использование ID Assertion для связи с пользователем помешает ли работоспособным discoverable credentials на устройствах Samsung?
-
Является ли эта проблема с null UserHandle специфичной для navigator.credentials.get() или она также затрагивает navigator.credentials.create()?
-
Может ли это быть связано с ограничениями Android в отношении discoverable credentials на security keys?
Конфигурация создания PassKey:
var createCredentialOptions = {
rp: { name: "WebAuthn Sample App", icon: "" },
user: {
id: stringToArrayBuffer("some.user.guid"),
name: "maherrj@gmail.com",
displayName: "Richard Maher",
icon: ""
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 }
],
authenticatorSelection: {
requireResidentKey: true,
userVerification: "discouraged",
authenticatorAttachment: "cross-platform"
},
timeout: 30000,
challenge: stringToArrayBuffer(serverChallenge.Token),
excludeCredentials: [],
attestation: "none"
};
Обработка Assertion:
return navigator.credentials
.get({ publicKey: getAssertionOptions })
.then((rawAssertion) => {
var assertion = {
id: base64encode(rawAssertion.rawId),
clientDataJSON: arrayBufferToString(rawAssertion.response.clientDataJSON),
userHandle: base64encode(rawAssertion.response.userHandle),
signature: base64encode(rawAssertion.response.signature),
authenticatorData: base64encode(rawAssertion.response.authenticatorData)
};
Мне нужна помощь в решении проблемы с null UserHandle и обеспечении надежной связи пользователей с PassKeys на разных устройствах, особенно на Samsung телефонах.
WebAuthn: Почему UserHandle равен null при использовании allowCredentials
UserHandle в WebAuthn постоянно равен null при использовании allowCredentials, поскольку спецификация требует такого поведения для обеспечения конфиденциальности пользователей и предотвращения отслеживания между сайтами. Когда вы указываете allowCredentials, вы запрашиваете аутентификацию от конкретных известных учетных данных, и браузер/платформа намеренно опускает UserHandle, чтобы предотвратить корреляцию учетных данных между разными сервисами. Для надежной ассоциации пользователей с PassKey следует использовать идентификатор учетных данных (rawId) в качестве основного идентификатора, так как он уникально идентифицирует каждые учетные данные, сохраняя при этом конфиденциальность пользователей.
Содержание
- Поведение null для UserHandle
- Особенности устройств Samsung
- Стратегии надежной ассоциации пользователей
- Влияние на обнаруживаемые учетные данные
- Операции Create vs Get
- Лучшие практики реализации
- Устранение распространенных проблем
Поведение null для UserHandle
Поведение null для UserHandle при использовании allowCredentials является намеренным и соответствует спецификации. Согласно спецификации WebAuthn, UserHandle опускается, когда:
- Используется параметр allowCredentials
- Аутентификатор не хранит информацию о пользователе для запрошенных учетных данных
- Конфиденциальность не позволяет раскрывать информацию о пользователе
“Идентификатор пользователя - это буфер байтов, который relying party может использовать для запоминания учетной записи пользователя, но он не предназначен для использования в качестве первичного ключа для учетной записи.” - Спецификация WebAuthn уровня 2
Этот выбор дизайна предотвращает для веб-сайтов:
- Корреляцию идентичности пользователя между разными сервисами
- Отслеживание пользователей на основе информации об учетных данных
- Создание профилей пользователей без явного согласия
Когда вы удаляете allowCredentials и запрашиваете “обнаруживаемые учетные данные” (ранее известные как резидентные ключи), UserHandle может быть заполнен, поскольку аутентификатор может предоставить информацию о пользователе, не раскрывая, какие именно учетные данные используются.
Особенности устройств Samsung
Устройства Samsung Galaxy с поддержкой PassKey часто демонстрируют более агрессивное поведение в отношении защиты конфиденциальности:
- Настройки конфиденциальности по умолчанию: Реализация Samsung отдает приоритет конфиденциальности пользователей, часто обнуляя UserHandle даже при создании резидентных ключей
- Ограничения платформы: Реализация WebAuthn в Android имеет различия между производителями, что влияет на обработку учетных данных
- Поведение ключей безопасности: При использовании Samsung Pass в качестве ключа безопасности поведение может отличаться от платформенных аутентификаторов
На основе распространенных шаблонов реализации:
- Устройства Samsung часто возвращают null для UserHandle независимо от настройки requireResidentKey
- Это более выражено, когда указан allowCredentials
- Поведение согласовано во всей серии Samsung Galaxy S, Note и складных устройствах
Рекомендация по тестированию: Тестируйте на нескольких моделях устройств Samsung и версиях Android, чтобы установить шаблон, поскольку поведение может различаться в реализациях Android 12, 13 и 14.
Стратегии надежной ассоциации пользователей
Когда UserHandle равен null, вам нужны альтернативные стратегии для ассоциации пользователей:
Основная стратегия: Использование идентификатора учетных данных (rawId)
Идентификатор учетных данных (rawId или id в вашем утверждении) является наиболее надежным идентификатором:
// Сохраняйте это при создании учетных данных
const credentialId = base64encode(rawCredential.rawId);
// Используйте это при аутентификации для поиска пользователя
const assertionId = base64encode(rawAssertion.rawId);
Пример схемы базы данных:
CREATE TABLE user_credentials (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
credential_id VARCHAR(255) NOT NULL UNIQUE,
public_key JSONB,
created_at TIMESTAMP,
last_used_at TIMESTAMP
);
Вторичная стратегия: Ассоциация на стороне клиента
Для усиления безопасности реализуйте ассоциацию на стороне клиента:
// При создании учетных данных
const createCredentialOptions = {
// ... существующие параметры
user: {
id: stringToArrayBuffer(userId), // ID пользователя в вашей системе
name: userEmail,
displayName: userDisplayName
}
};
// Сохраняйте сопоставление: credentialId -> userId
Запасная стратегия: Сопоставление “запрос-ответ”
Для крайних случаев реализуйте временную ассоциацию на основе запроса:
// Временно сохраняйте сопоставление запроса -> userId
const challengeStore = new Map();
// При утверждении
const userId = challengeStore.get(challenge);
if (userId) {
// Ассоциируйте утверждение с этим пользователем
}
Влияние на обнаруживаемые учетные данные
Использование идентификатора учетных данных для ассоциации пользователей не предотвращает работу обнаруживаемых учетных данных на устройствах Samsung. Вот почему:
- Обнаруживаемые учетные данные все равно работают - пользователь все равно может выбрать, какие учетные данные использовать
- Идентификатор учетных данных остается стабильным - он уникально идентифицирует каждые учетные данные независимо от UserHandle
- Поток аутентификации остается неизменным - процесс проверки подписи работает идентично
Ключевые моменты:
- Обнаруживаемые учетные данные позволяют пользователям проходить аутентификацию без указания конкретных учетных данных
- Идентификатор учетных данных все еще доступен для вашего сервера для идентификации конкретных учетных данных
- Пользовательский опыт остается таким же для конечных пользователей
Тестирование обнаруживаемых учетных данных:
// Тестируйте оба сценария
const optionsWithAllowCredentials = {
allowCredentials: [{ id: knownCredentialId }]
};
const optionsWithoutAllowCredentials = {
// Запрашиваем обнаруживаемые учетные данные
};
Операции Create vs Get
Поведение UserHandle различается между операциями credentials.create() и credentials.get():
Операция credentials.create()
- UserHandle обычно заполняется при создании резидентных ключей
requireResidentKey: trueувеличивает вероятность наличия UserHandle- Устройства Samsung все равно могут возвращать null для UserHandle при создании
Операция credentials.get()
- UserHandle равен null, когда указан allowCredentials
- UserHandle может быть доступен, когда запрашиваются обнаруживаемые учетные данные
- Поведение более согласовано между устройствами при операциях get
Важное замечание: UserHandle, возвращаемый при операциях create, не гарантированно будет таким же, как то, что может быть возвращено при операциях get, даже для одних и тех же учетных данных.
Лучшие практики реализации
1. Всегда используйте идентификатор учетных данных в качестве основного идентификатора
// Обработка утверждения
function processAssertion(rawAssertion) {
return {
id: base64encode(rawAssertion.rawId), // Основной идентификатор
clientDataJSON: arrayBufferToString(rawAssertion.response.clientDataJSON),
userHandle: base64encode(rawAssertion.response.userHandle || ''), // Запасной вариант - пустая строка
signature: base64encode(rawAssertion.response.signature),
authenticatorData: base64encode(rawAssertion.response.authenticatorData),
userId: null // Будет заполнен при поиске учетных данных
};
}
2. Реализуйте плавные запасные варианты
// Обрабатывайте оба сценария
async function authenticateUser(challenge, allowCredentials = null) {
const options = {
challenge: challenge,
timeout: 60000
};
if (allowCredentials) {
options.allowCredentials = allowCredentials;
}
try {
const assertion = await navigator.credentials.get({ publicKey: options });
return processAssertion(assertion);
} catch (err) {
console.error('Аутентификация не удалась:', err);
throw err;
}
}
3. Проектирование базы данных для нескольких учетных данных
// Поддержка нескольких учетных данных на пользователя
const userCredentialSchema = {
userId: String,
credentials: [{
credentialId: String,
publicKey: String,
counter: Number,
deviceInfo: {
type: String,
authenticatorAttachment: String,
aaguid: String
},
createdAt: Date,
lastUsedAt: Date
}]
};
Устранение распространенных проблем
Проблема: UserHandle равен null на всех устройствах
Возможные причины:
- Указан параметр allowCredentials
- Резидентный ключ не был создан правильно
- Настройки конфиденциальности в браузере
Решения:
- Сначала протестируйте без allowCredentials
- Проверьте параметры создания резидентного ключа
- Проверьте настройки конфиденциальности в браузере
Проблема: Идентификатор учетных данных не согласован
Возможные причины:
- Разные аутентификаторы возвращают разные форматы ID
- Проблемы с кодированием данных
Решения:
- Стандартизируйте кодирование (всегда используйте base64url)
- Сохраняйте rawId как ArrayBuffer в базе данных
- Реализуйте утилиты правильного кодирования/декодирования
Проблема: Сбои, специфичные для устройств Samsung
Возможные причины:
- Агрессивная реализация конфиденциальности Samsung
- Различия в версиях Android
- Проблемы с конфигурацией Samsung Pass
Решения:
- Тестируйте на нескольких устройствах Samsung
- Реализуйте специфичную для устройств обработку, если необходимо
- Рассмотрите запасные методы аутентификации
Источники
- Спецификация WebAuthn уровня 2 - Идентификатор пользователя
- Руководство по реализации WebAuthn - Mozilla Developer Network
- Лучшие практики Android WebAuthn - Google Developers
- Документация разработчика Samsung Pass
- Обзор WebAuthn от FIDO Alliance
Заключение
Поведение null для UserHandle при использовании allowCredentials является преднамеренной функцией конфиденциальности спецификации WebAuthn, а не ошибкой. Чтобы обеспечить надежную ассоциацию пользователей на всех устройствах, особенно на телефонах Samsung:
- Используйте идентификатор учетных данных (rawId) в качестве основного идентификатора - он стабильный, уникальный и доступен независимо от статуса UserHandle
- Проектируйте свою базу данных для обработки нескольких учетных данных на пользователя - это позволяет учитывать сценарии как с allowCredentials, так и с обнаруживаемыми учетными данными
- Реализуйте плавные запасные варианты - обрабатывайте как сценарии с null, так и с заполненным UserHandle
- Тестируйте на различных типах устройств - особенно на устройствах Samsung, которые имеют более агрессивное поведение в отношении конфиденциальности
- Рассмотрите ассоциацию на стороне клиента - при возможности безопасно храните сопоставления пользователей на стороне клиента
Следуя этим стратегиям, вы можете создать надежную систему аутентификации PassKey, которая работает согласованно на всех устройствах, сохраняя при этом конфиденциальность и безопасность пользователей. Помните, что идентификатор учетных данных остается наиболее надежным идентификатором для ассоциации пользователей в реализациях WebAuthn.