В этом руководстве подробно рассказывается, как реализовать пошаговую аутентификацию или повторную проверку для вошедшего в систему пользователя, прежде чем он внесет конфиденциальные изменения в учетную запись (например, обновит пароль, выполнит финансовый перевод).
Архитектура Delta Flow
В отличие от обычной аутентификации, повторная аутентификация с помощью ключа доступа ограничивает диалоговые окна ключа доступа строго предварительно зарегистрированными учетными данными вошедшего в систему пользователя, чтобы предотвратить смешивание аккаунтов или подмену ключа доступа во время активных сеансов.
Серверный
Разница в количестве сгенерированных вариантов
Создайте конечную точку, которая заполняет параметры разрешенных учетных данных специально для активного известного пользователя:
Ограничение учетных данных. Заполните массив параметров allowCredentials определенными записями PublicKeyCredentialDescriptor, сопоставив все зарегистрированные идентификаторы учетных данных для вошедшего в систему пользователя. Если оставить это поле пустым или пропустить его, будут использоваться обнаруживаемые учетные данные, что нарушает безопасность сеанса.
// Node.js step-up options generation example
router.post("/api/reauth/options", enforceActiveSession, async (req, res) => {
const userPasskeys = await db.findCredentialsByUserId(req.user.id);
const options = {
challenge: serverGeneratedBase64UrlChallenge, // Random challenge stored in user session
rpId: "example.com",
// Enforce allowance strictly limited to the user's credentials list
allowCredentials: userPasskeys.map((cred) => ({
type: "public-key",
id: cred.id,
transports: cred.transports, // Speeds up resolution by indicating platform transports
})),
};
return res.json(options);
});
Разница между конечными точками проверки
Проверьте утверждение, возвращенное клиентом:
Подтверждение права собственности на аккаунт. Конечная точка подтверждения ДОЛЖНА явно подтверждать, что полученный в результате аутентификации идентификатор учетных данных, возвращенный клиентом, разрешается в сохраненную запись учетных данных, связанный идентификатор пользователя которой строго соответствует активному вошедшему в систему пользователю (storedCredential.passkeyUserId === req.user.id). Если возвращается действительный ключ доступа другого пользователя, аутентификация ДОЛЖНА быть немедленно отклонена.
Различия в процессах на стороне клиента
Приложения выбирают один из двух интерфейсов повторной аутентификации в зависимости от интерфейса транзакции:
A. Кнопка (без полей ввода)
Запускайте повторную аутентификацию, когда пользователь нажимает кнопку "Подтвердить личность" или "Продолжить транзакцию".
<button id="reauth-btn" data-testid="reauth-button">Confirm Transaction</button>
let reauthAbortController = new AbortController();
async function triggerButtonReauth() {
// Abort any background suggestion flows to avoid passkey prompt collisions
reauthAbortController.abort();
reauthAbortController = new AbortController();
const optionsResponse = await fetch("/api/reauth/options", {
method: "POST",
});
const optionsJSON = await optionsResponse.json();
const publicKey =
PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);
try {
const credential = await navigator.credentials.get({
publicKey,
signal: reauthAbortController.signal,
});
if (credential) {
const encodedCredential = credential.toJSON();
const verifyResponse = await fetch("/api/reauth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(encodedCredential),
});
if (verifyResponse.ok) {
showTransactionSuccessUI();
} else if (verifyResponse.status === 404 && PublicKeyCredential.signalUnknownCredential) {
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encodedCredential.id
});
}
}
} catch (err) {
if (err.name === "NotAllowedError") {
console.log("User cancelled reauthentication.");
}
}
}
document
.getElementById("reauth-btn")
.addEventListener("click", triggerButtonReauth);
Резервные стратегии
Резервный вариант обнаружения функции ключей доступа
Всегда устанавливайте webauthn-polyfills и импортируйте его в контекст.
Учтите, что если поддерживается PublicKeyCredential, то поддерживается и PublicKeyCredential.getClientCapabilities.
import 'webauthn-polyfills';
Простая резервная сериализация JSON
Всегда устанавливайте webauthn-polyfills и импортируйте его в контекст.
Если поддерживается PublicKeyCredential, то поддерживаются и PublicKeyCredential.parseRequestOptionsFromJSON, и PublicKeyCredential.prototype.toJSON.
import 'webauthn-polyfills';