Обзор
Вот общий обзор ключевых шагов, связанных с аутентификацией с помощью пароля:
- Определите задачу и другие параметры, необходимые для аутентификации с помощью ключа доступа. Отправьте их клиенту, чтобы вы могли передать их при вызове аутентификации по ключу доступа (
navigator.credentials.get
в Интернете). После того как пользователь подтверждает аутентификацию с помощью ключа доступа, вызов проверки подлинности с помощью ключа доступа разрешается и возвращает учетные данные (PublicKeyCredential
). Учетные данные содержат утверждение аутентификации .
- Проверьте утверждение аутентификации.
- Если утверждение аутентификации действительно, аутентифицируйте пользователя.
В следующих разделах рассматриваются особенности каждого шага.
Создайте вызов
На практике вызов представляет собой массив случайных байтов, представленный в виде объекта ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
Чтобы убедиться, что задание достигает своей цели, вы должны:
- Убедитесь, что один и тот же вызов никогда не используется более одного раза. Создавайте новый вызов при каждой попытке входа в систему. Отменяйте вызов после каждой попытки входа в систему, независимо от того, успешна она или нет. Также отмените вызов по истечении определенного времени. Никогда не принимайте один и тот же вызов в ответ более одного раза.
- Убедитесь, что задача криптографически безопасна . Задача должна быть практически невозможно угадать . Чтобы создать криптографически безопасный вызов на стороне сервера, лучше всего положиться на серверную библиотеку FIDO, которой вы доверяете. Если вместо этого вы создаете свои собственные задачи, используйте встроенные криптографические функции, доступные в вашем технологическом стеке, или ищите библиотеки, предназначенные для сценариев криптографического использования. Примеры включают iso-crypto в Node.js или секреты в Python. Согласно спецификации , длина запроса должна быть не менее 16 байт, чтобы считаться безопасным.
Создав задачу, сохраните ее в сеансе пользователя, чтобы проверить ее позже.
Создание параметров запроса учетных данных
Создайте параметры запроса учетных данных как объект publicKeyCredentialRequestOptions
.
Для этого положитесь на свою серверную библиотеку FIDO. Обычно он предлагает служебную функцию, которая может создать для вас эти параметры. SimpleWebAuthn предлагает, например, generateAuthenticationOptions
.
publicKeyCredentialRequestOptions
должен содержать всю информацию, необходимую для аутентификации с помощью ключа доступа. Передайте эту информацию функции в вашей серверной библиотеке FIDO, которая отвечает за создание объекта publicKeyCredentialRequestOptions
.
Некоторые поля publicKeyCredentialRequestOptions
могут быть константами. Другие должны быть динамически определены на сервере:
-
rpId
: с каким идентификатором RP, по вашему мнению, будут связаны учетные данные, напримерexample.com
. Аутентификация будет успешной только в том случае, если предоставленный вами здесь идентификатор RP соответствует идентификатору RP, связанному с учетными данными. Чтобы заполнить идентификатор RP, используйте то же значение, что и идентификатор RP, который вы установили вpublicKeyCredentialCreationOptions
во время регистрации учетных данных. -
challenge
: часть данных, которую поставщик ключа доступа подпишет, чтобы доказать, что пользователь владеет ключом доступа во время запроса аутентификации. Подробности см. в разделе «Создание соревнования» . -
allowCredentials
: Массив допустимых учетных данных для этой аутентификации. Передайте пустой массив, чтобы позволить пользователю выбрать доступный ключ доступа из списка, отображаемого браузером. Подробные сведения см. в разделе «Получение запроса с сервера RP» и «Обнаруживаемые учетные данные» . -
userVerification
: указывает, является ли проверка пользователя с помощью блокировки экрана устройства «обязательной», «предпочтительной» или «не рекомендуется». Просмотрите Получение вызова с сервера RP . -
timeout
: сколько времени (в миллисекундах) может потребоваться пользователю для завершения аутентификации. Оно должно быть достаточно щедрым и короче, чем время существованияchallenge
. Рекомендуемое значение по умолчанию — 5 минут , но вы можете увеличить его — до 10 минут, что все равно находится в рекомендуемом диапазоне . Длинные тайм-ауты имеют смысл, если вы ожидаете, что пользователи будут использовать гибридный рабочий процесс , который обычно занимает немного больше времени. Если время выполнения операции истечет, будет выданоNotAllowedError
.
После создания publicKeyCredentialRequestOptions
отправьте его клиенту.
Пример кода: создание параметров запроса учетных данных
В наших примерах мы используем библиотеку SimpleWebAuthn . Здесь мы передаем создание параметров запроса учетных данных generateAuthenticationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
router.post('/signinRequest', csrfCheck, async (req, res) => {
// Ensure you nest calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Use the generateAuthenticationOptions function from SimpleWebAuthn
const options = await generateAuthenticationOptions({
rpID: process.env.HOSTNAME,
allowCredentials: [],
});
// Save the challenge in the user session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Подтвердите и войдите в систему пользователя
Когда navigator.credentials.get
успешно разрешается на клиенте, он возвращает объект PublicKeyCredential
.
response
является AuthenticatorAssertionResponse
. Он представляет собой ответ поставщика ключа доступа на инструкцию клиента создать то, что необходимо для попытки аутентификации с помощью ключа доступа на RP. Он содержит:
-
response.authenticatorData
иresponse.clientDataJSON
, как на этапе регистрации ключа доступа . -
response.signature
, который содержит подпись над этими значениями.
Отправьте объект PublicKeyCredential
на сервер.
На сервере сделайте следующее:
- Соберите информацию, необходимую для проверки утверждения и аутентификации пользователя:
- Получите ожидаемый запрос, который вы сохранили в сеансе при создании параметров аутентификации .
- Получите ожидаемый источник и идентификатор RP.
- Найдите в своей базе данных, кто этот пользователь. В случае обнаруживаемых учетных данных вы не знаете, кто является пользователем, отправляющим запрос на аутентификацию. Чтобы это узнать, у вас есть два варианта:
- Вариант 1. Используйте
response.userHandle
в объектеPublicKeyCredential
. В таблице «Пользователи» найдитеpasskey_user_id
, соответствующийuserHandle
. - Вариант 2. Используйте
id
учетных данных, присутствующий в объектеPublicKeyCredential
. В таблице учетных данных открытого ключа найдитеid
учетных данных, который соответствуетid
учетных данных, присутствующему в объектеPublicKeyCredential
. Затем найдите соответствующего пользователя, используя внешний ключpasskey_user_id
в вашей таблице «Пользователи» .
- Вариант 1. Используйте
- Найдите в своей базе данных информацию об учетных данных открытого ключа, соответствующую полученному вами утверждению аутентификации. Для этого в таблице учетных данных открытого ключа найдите
id
учетных данных, соответствующийid
учетных данных, присутствующему в объектеPublicKeyCredential
.
Проверьте утверждение аутентификации. Передайте этот этап проверки серверной библиотеке FIDO, которая обычно предлагает для этой цели служебную функцию. SimpleWebAuthn предлагает, например,
verifyAuthenticationResponse
. Узнайте, что происходит «под капотом», в Приложении: проверка ответа на аутентификацию .Удалите запрос, была ли проверка успешной или нет , чтобы предотвратить атаки повторного воспроизведения.
Войдите в систему. Если проверка прошла успешно, обновите информацию о сеансе, чтобы отметить пользователя как вошедшего в систему. Вы также можете захотеть вернуть объект
user
клиенту, чтобы интерфейс мог использовать информацию, связанную с недавно вошедшим в систему пользователем.
Пример кода: проверка и вход пользователя
В наших примерах мы используем библиотеку SimpleWebAuthn . Здесь мы передаем проверку ответа аутентификации verifyAuthenticationResponse
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/signinResponse', csrfCheck, async (req, res) => {
const response = req.body;
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Find the credential stored to the database by the credential ID
const cred = Credentials.findById(response.id);
if (!cred) {
throw new Error('Credential not found.');
}
// Find the user - Here alternatively we could look up the user directly
// in the Users table via userHandle
const user = Users.findByPasskeyUserId(cred.passkey_user_id);
if (!user) {
throw new Error('User not found.');
}
// Base64URL decode some values
const authenticator = {
credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
credentialID: isoBase64URL.toBuffer(cred.id),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
requireUserVerification: false,
});
if (!verified) {
throw new Error('User verification failed.');
}
// Kill the challenge for this session.
delete req.session.challenge;
req.session.username = user.username;
req.session['signed-in'] = 'yes';
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Приложение: проверка ответа аутентификации
Проверка ответа аутентификации состоит из следующих проверок:
- Убедитесь, что идентификатор RP соответствует вашему сайту.
- Убедитесь, что источник запроса совпадает с источником входа на ваш сайт. Для приложений Android ознакомьтесь с разделом «Подтвердить происхождение» .
- Убедитесь, что устройство способно выполнить поставленную вами задачу.
- Убедитесь, что во время аутентификации пользователь выполнил требования, которые вы установили как RP. Если вам требуется проверка пользователя, убедитесь, что флаг
uv
(проверено пользователем) вauthenticatorData
имеетtrue
. Убедитесь, что флагup
(присутствие пользователя) вauthenticatorData
имеет значениеtrue
, поскольку присутствие пользователя всегда требуется для ключей доступа. - Проверьте подпись. Для проверки подписи необходимо:
- Подпись, которая представляет собой подписанный запрос:
response.signature
- Открытый ключ для проверки подписи.
- Исходные подписанные данные. Это данные, подпись которых необходимо проверить.
- Криптографический алгоритм, который использовался для создания подписи.
- Подпись, которая представляет собой подписанный запрос:
Чтобы узнать больше об этих шагах, проверьте исходный код SimpleWebAuthn на verifyAuthenticationResponse
или ознакомьтесь с полным списком проверок в спецификации .