1. Прежде чем начать
Использование паролей вместо ключей доступа — отличный способ сделать учётные записи пользователей на веб-сайтах безопаснее, проще и удобнее. С помощью ключа доступа пользователь может войти на веб-сайт или в приложение, используя функцию блокировки экрана устройства, например, отпечаток пальца, распознавание лица или PIN-код устройства. Ключ доступа необходимо создать, связать с учётной записью пользователя и сохранить его открытый ключ на сервере, прежде чем пользователь сможет войти с его помощью.
В этой лабораторной работе вы превратите базовую форму входа с использованием имени пользователя и пароля в систему, которая поддерживает ключи доступа и включает в себя следующее:
- Кнопка, которая создает ключ доступа после входа пользователя в систему.
- Пользовательский интерфейс, отображающий список зарегистрированных ключей доступа.
- Существующая форма входа, которая позволяет пользователям входить в систему с зарегистрированным паролем через автозаполнение форм.
Предпосылки
- Базовое понимание JavaScript
- Базовое понимание паролей
- Базовое понимание API веб-аутентификации (WebAuthn)
Чему вы научитесь
- Как создать ключ доступа.
- Как аутентифицировать пользователей с помощью ключа доступа.
- Как разрешить форме предлагать пароль в качестве варианта входа.
Что вам понадобится
Одна из следующих комбинаций устройств:
- Google Chrome с устройством Android под управлением Android 9 или выше, желательно с биометрическим датчиком.
- Chrome с устройством Windows под управлением Windows 10 или выше.
- Safari 16 или выше с iPhone под управлением iOS 16 или выше или iPad под управлением iPadOS 16 или выше.
- Safari 16 или выше или Chrome с настольным устройством Apple под управлением macOS Ventura или выше.
2. Настройте
В этой лабораторной работе вы используете сервис Glitch, который позволяет редактировать клиентский и серверный код с помощью JavaScript и развертывать его исключительно из браузера.
Открыть проект
- Откройте проект в Glitch .
- Нажмите Remix , чтобы создать ответвление проекта Glitch.
- В навигационном меню в нижней части Glitch нажмите «Предварительный просмотр» > «Предварительный просмотр в новом окне» . В браузере откроется ещё одна вкладка.
Проверьте начальное состояние веб-сайта
- На вкладке предварительного просмотра введите случайное имя пользователя и нажмите кнопку Далее .
- Введите случайный пароль и нажмите «Войти» . Пароль будет проигнорирован, но вы всё равно пройдете аутентификацию и окажетесь на главной странице.
- Если вы хотите изменить отображаемое имя, сделайте это. Это всё, что вы можете сделать в исходном состоянии.
- Нажмите Выйти .
В этом состоянии пользователи должны вводить пароль при каждом входе в систему. Вы добавляете поддержку ключа доступа в эту форму, чтобы пользователи могли входить в систему, используя функцию блокировки экрана устройства. Вы можете попробовать конечное состояние по адресу https://passkeys-codelab.glitch.me/ .
Дополнительную информацию о работе паролей см. в разделе Как работают пароли?.
3. Добавить возможность создания ключа доступа.
Чтобы разрешить пользователям проходить аутентификацию с помощью ключа доступа, необходимо предоставить им возможность создавать и регистрировать ключ доступа, а также хранить его открытый ключ на сервере.
Вы хотите разрешить создание ключа доступа после входа пользователя с паролем и добавить пользовательский интерфейс, позволяющий пользователям создавать ключ доступа и просматривать список всех зарегистрированных ключей доступа на странице /home
. В следующем разделе вы создадите функцию, которая создаёт и регистрирует ключ доступа.
Создайте функцию registerCredential()
- В Glitch перейдите к файлу
public/client.js
и прокрутите его до конца. - После соответствующего комментария добавьте следующую функцию
registerCredential()
:
public /client.js
// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
// TODO: Add an ability to create a passkey: Create a credential.
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
};
Эта функция создает и регистрирует ключ доступа на сервере.
Получите вызов и другие параметры с конечной точки сервера
Перед созданием ключа доступа необходимо запросить у сервера параметры для передачи в WebAuthn , включая запрос. WebAuthn — это API браузера, позволяющий пользователю создать ключ доступа и аутентифицировать его с его помощью. К счастью, у вас уже есть конечная точка сервера, которая отвечает такими параметрами в этой лабораторной работе.
- Чтобы получить вызов и другие параметры с конечной точки сервера, добавьте следующий код в тело функции
registerCredential()
после соответствующего комментария:
public/client.js
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/registerRequest');
Следующий фрагмент кода содержит примеры параметров, которые вы получаете от сервера:
{
challenge: *****,
rp: {
id: "example.com",
},
user: {
id: *****,
name: "john78",
displayName: "John",
},
pubKeyCredParams: [{
alg: -7, type: "public-key"
},{
alg: -257, type: "public-key"
}],
excludeCredentials: [{
id: *****,
type: 'public-key',
transports: ['internal', 'hybrid'],
}],
authenticatorSelection: {
authenticatorAttachment: "platform",
requireResidentKey: true,
}
}
Протокол между сервером и клиентом не входит в спецификацию WebAuthn . Однако сервер в этой лабораторной работе предназначен для возврата JSON-кода, максимально похожего на словарь PublicKeyCredentialCreationOptions
, передаваемый в API WebAuthn navigator.credentials.create()
.
Следующая таблица не является исчерпывающей, но она содержит важные параметры словаря PublicKeyCredentialCreationOptions
:
Параметры | Описания |
Сгенерированный сервером запрос в объекте | |
Уникальный идентификатор пользователя. Это значение должно быть объектом | |
Это поле должно содержать уникальный идентификатор учётной записи, узнаваемый пользователем, например, адрес электронной почты или имя пользователя. Он отображается в селекторе учётных записей. (Если вы используете имя пользователя, используйте то же значение, что и при аутентификации по паролю.) | |
Это поле — необязательное, удобное для пользователя имя учётной записи. Оно не обязательно должно быть уникальным и может быть выбранным пользователем. Если на вашем сайте нет подходящего значения для этого поля, передайте пустую строку. В зависимости от браузера, это имя может отображаться в селекторе учётных записей. | |
Идентификатор проверяющей стороны (RP) — это домен. Веб-сайт может указать либо свой домен, либо регистрируемый суффикс . Например, если происхождение RP — https://login.example.com:1337, идентификатор RP может быть | |
В этом поле указываются поддерживаемые RP алгоритмы с открытым ключом. Мы рекомендуем установить его в значение | |
Предоставляет список уже зарегистрированных идентификаторов учётных данных, чтобы предотвратить двойную регистрацию одного и того же устройства. При наличии, элемент | |
Установите значение | |
Установите логическое значение | |
Установите |
Создать учетные данные
- В теле функции
registerCredential()
после соответствующего комментария преобразуйте некоторые параметры, закодированные с помощью Base64URL, обратно в двоичный формат, в частности строкиuser.id
иchallenge
, а также экземпляры строкиid
, включенные в массивexcludeCredentials
:
public/client.js
// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);
if (options.excludeCredentials) {
for (let cred of options.excludeCredentials) {
cred.id = base64url.decode(cred.id);
}
}
- В следующей строке установите для
authenticatorSelection.authenticatorAttachment
значение"platform"
, аauthenticatorSelection.requireResidentKey
— значениеtrue
. Это позволит использовать только платформенный аутентификатор (само устройство) с возможностью обнаружения учётных данных.
public/client.js
// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
authenticatorAttachment: 'platform',
requireResidentKey: true
}
- На следующей строке вызовите метод
navigator.credentials.create()
для создания учетных данных.
public/client.js
// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
publicKey: options,
});
При этом вызове браузер пытается проверить личность пользователя с помощью блокировки экрана устройства.
Зарегистрируйте учетные данные на конечной точке сервера.
После подтверждения личности пользователя создаётся и сохраняется ключ доступа. Веб-сайт получает объект учётных данных, содержащий открытый ключ, который можно отправить на сервер для регистрации ключа доступа.
Следующий фрагмент кода содержит пример объекта учетных данных:
{
"id": *****,
"rawId": *****,
"type": "public-key",
"response": {
"clientDataJSON": *****,
"attestationObject": *****,
"transports": ["internal", "hybrid"]
},
"authenticatorAttachment": "platform"
}
Следующая таблица не является исчерпывающей, но она содержит важные параметры объекта PublicKeyCredential
:
Параметры | Описания |
Идентификатор созданного ключа доступа, закодированный в формате Base64URL. Этот идентификатор помогает браузеру определить, есть ли соответствующий ключ доступа на устройстве при аутентификации. Это значение должно храниться в базе данных на сервере. | |
Версия идентификатора учетных данных объекта | |
Объект | |
Объект аттестации, закодированный в | |
Список транспортов, поддерживаемых устройством: | |
Возвращает |
Чтобы отправить объект учетных данных на сервер, выполните следующие действия:
- Закодируйте двоичные параметры учетных данных как Base64URL, чтобы их можно было доставить на сервер в виде строки:
public/client.js
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
credential.authenticatorAttachment = cred.authenticatorAttachment;
}
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);
// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];
credential.response = {
clientDataJSON,
attestationObject,
transports
};
- На следующей строке отправьте объект на сервер:
public/client.js
return await _fetch('/auth/registerResponse', credential);
При запуске программы сервер возвращает HTTP code 200
, который указывает на то, что учетные данные зарегистрированы.
Теперь у вас есть полная функция registerCredential()
!
Проверьте код решения для этого раздела.
public/client.js
// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from server endpoint.
const options = await _fetch('/auth/registerRequest');
// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);
if (options.excludeCredentials) {
for (let cred of options.excludeCredentials) {
cred.id = base64url.decode(cred.id);
}
}
// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
authenticatorAttachment: 'platform',
requireResidentKey: true
}
// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
publicKey: options,
});
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
credential.authenticatorAttachment = cred.authenticatorAttachment;
}
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject =
base64url.encode(cred.response.attestationObject);
// Obtain transports.
const transports = cred.response.getTransports ?
cred.response.getTransports() : [];
credential.response = {
clientDataJSON,
attestationObject,
transports
};
return await _fetch('/auth/registerResponse', credential);
};
4. Создайте пользовательский интерфейс для регистрации и управления учетными данными ключей доступа.
Теперь, когда функция registerCredential()
доступна, вам нужна кнопка для её вызова. Кроме того, вам нужно отобразить список зарегистрированных паролей.
Добавить HTML-заполнитель
- В Glitch перейдите к файлу
views/home.html
. - После соответствующего комментария добавьте заполнитель пользовательского интерфейса, который отображает кнопку для регистрации ключа доступа и список ключей доступа:
views/home.html
<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
<h3 class="mdc-typography mdc-typography--headline6"> Your registered
passkeys:</h3>
<div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>
Элемент div#list
является заполнителем для списка.
Проверьте наличие поддержки ключа доступа
Чтобы возможность создания ключа доступа отображалась только для пользователей с устройствами, поддерживающими ключи доступа, сначала необходимо проверить, доступен ли WebAuthn. Если да, то необходимо удалить hidden
класс, чтобы кнопка «Создать ключ доступа» отображалась.
Чтобы проверить, поддерживает ли среда ключи доступа, выполните следующие действия:
- В конце файла
views/home.html
после соответствующего комментария напишите условие, которое выполнится, еслиwindow.PublicKeyCredential
,PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable
иPublicKeyCredential.isConditionalMediationAvailable
равныtrue
.
views/home.html
// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
PublicKeyCredential.isConditionalMediationAvailable) {
- В теле условного выражения проверьте, может ли устройство создать ключ доступа, а затем проверьте, может ли ключ доступа быть предложен при автозаполнении формы.
views/home.html
try {
const results = await Promise.all([
// Is platform authenticator available in this browser?
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
// Is conditional UI available in this browser?
PublicKeyCredential.isConditionalMediationAvailable()
]);
- Если все условия выполнены, показать кнопку для создания ключа доступа. В противном случае вывести предупреждение.
views/home.html
if (results.every(r => r === true)) {
// If conditional UI is available, reveal the Create a passkey button.
createPasskey.classList.remove('hidden');
} else {
// If conditional UI isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
} catch (e) {
console.error(e);
}
} else {
// If WebAuthn isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
Отобразить зарегистрированные ключи доступа в списке
- Определите функцию
renderCredentials()
, которая извлекает зарегистрированные ключи доступа с сервера и отображает их в виде списка. К счастью, у вас уже есть конечная точка сервера/auth/getKeys
для извлечения зарегистрированных ключей доступа для вошедшего в систему пользователя.
views/home.html
// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
const res = await _fetch('/auth/getKeys');
const list = $('#list');
const creds = html`${res.length > 0 ? html`
<mwc-list>
${res.map(cred => html`
<mwc-list-item>
<div class="list-item">
<div class="entity-name">
<span>${cred.name || 'Unnamed' }</span>
</div>
<div class="buttons">
<mwc-icon-button data-cred-id="${cred.id}"
data-name="${cred.name || 'Unnamed' }" @click="${rename}"
icon="edit"></mwc-icon-button>
<mwc-icon-button data-cred-id="${cred.id}" @click="${remove}"
icon="delete"></mwc-icon-button>
</div>
</div>
</mwc-list-item>`)}
</mwc-list>` : html`
<mwc-list>
<mwc-list-item>No credentials found.</mwc-list-item>
</mwc-list>`}`;
render(creds, list);
};
- На следующей строке вызовите функцию
renderCredentials()
для отображения зарегистрированных ключей доступа, как только пользователь перейдет на страницу/home
в качестве инициализации.
views/home.html
renderCredentials();
Создайте и зарегистрируйте ключ доступа
Чтобы создать и зарегистрировать ключ доступа, вам необходимо вызвать функцию registerCredential()
, которую вы реализовали ранее.
Чтобы вызвать функцию registerCredential()
при нажатии кнопки «Создать ключ доступа» , выполните следующие действия:
- В файле после HTML-заполнителя найдите следующий оператор
import
:
views/home.html
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
} from '/client.js';
- В конце тела оператора
import
добавьте функциюregisterCredential()
.
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
registerCredential
} from '/client.js';
- В конце файла, после соответствующего комментария, определите функцию
register()
, которая вызывает функциюregisterCredential()
и загружает пользовательский интерфейс, а также вызываетrenderCredentials()
после регистрации. Это поясняет, что браузер создаёт ключ доступа и выводит сообщение об ошибке в случае возникновения проблем.
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
try {
// Start the loading UI.
loading.start();
// Start creating a passkey.
await registerCredential();
// Stop the loading UI.
loading.stop();
// Render the updated passkey list.
renderCredentials();
- В теле функции
register()
перехватывайте исключения. Методnavigator.credentials.create()
выдаёт ошибкуInvalidStateError
, если ключ доступа уже существует на устройстве. Это проверяется с помощью массиваexcludeCredentials
. В этом случае вы показываете пользователю соответствующее сообщение. Он также выдаёт ошибкуNotAllowedError
, когда пользователь отменяет диалог аутентификации. В этом случае вы просто игнорируете её.
views/home.html
} catch (e) {
// Stop the loading UI.
loading.stop();
// An InvalidStateError indicates that a passkey already exists on the device.
if (e.name === 'InvalidStateError') {
alert('A passkey already exists for this device.');
// A NotAllowedError indicates that the user canceled the operation.
} else if (e.name === 'NotAllowedError') {
Return;
// Show other errors in an alert.
} else {
alert(e.message);
console.error(e);
}
}
};
- В строке после функции
register()
присоедините функциюregister()
к событиюclick
для кнопки « Создать ключ доступа» .
views/home.html
createPasskey.addEventListener('click', register);
Проверьте код решения для этого раздела.
views/home.html
<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
<h3 class="mdc-typography mdc-typography--headline6"> Your registered
passkeys:</h3>
<div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
registerCredential
} from '/client.js';
views/home.html
// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
PublicKeyCredential.isConditionalMediationAvailable) {
try {
const results = await Promise.all([
// Is platform authenticator available in this browser?
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
// Is conditional UI available in this browser?
PublicKeyCredential.isConditionalMediationAvailable()
]);
if (results.every(r => r === true)) {
// If conditional UI is available, reveal the Create a passkey button.
createPasskey.classList.remove('hidden');
} else {
// If conditional UI isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
} catch (e) {
console.error(e);
}
} else {
// If WebAuthn isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
const res = await _fetch('/auth/getKeys');
const list = $('#list');
const creds = html`${res.length > 0 ? html`
<mwc-list>
${res.map(cred => html`
<mwc-list-item>
<div class="list-item">
<div class="entity-name">
<span>${cred.name || 'Unnamed' }</span>
</div>
<div class="buttons">
<mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
<mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
</div>
</div>
</mwc-list-item>`)}
</mwc-list>` : html`
<mwc-list>
<mwc-list-item>No credentials found.</mwc-list-item>
</mwc-list>`}`;
render(creds, list);
};
renderCredentials();
// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
try {
// Start the loading UI.
loading.start();
// Start creating a passkey.
await registerCredential();
// Stop the loading UI.
loading.stop();
// Render the updated passkey list.
renderCredentials();
} catch (e) {
// Stop the loading UI.
loading.stop();
// An InvalidStateError indicates that a passkey already exists on the device.
if (e.name === 'InvalidStateError') {
alert('A passkey already exists for this device.');
// A NotAllowedError indicates that the user canceled the operation.
} else if (e.name === 'NotAllowedError') {
Return;
// Show other errors in an alert.
} else {
alert(e.message);
console.error(e);
}
}
};
createPasskey.addEventListener('click', register);
Попробуй это
Если вы выполнили все шаги до сих пор, вы реализовали возможность создания, регистрации и отображения паролей на веб-сайте!
Чтобы попробовать, выполните следующие действия:
- На вкладке предварительного просмотра войдите в систему, используя случайное имя пользователя и пароль.
- Нажмите Создать ключ доступа .
- Подтвердите свою личность с помощью блокировки экрана устройства.
- Подтвердите, что ключ доступа зарегистрирован и отображается в разделе «Ваши зарегистрированные ключи доступа» на веб-странице.
Переименовать и удалить зарегистрированные ключи доступа
Вы должны иметь возможность переименовывать или удалять зарегистрированные пароли в списке. Вы можете проверить, как это работает, в коде, который прилагается к практической работе.
В Chrome вы можете удалить зарегистрированные пароли из chrome://settings/passkeys на десктопе или из менеджера паролей в настройках на Android.
Информацию о том, как переименовывать и удалять зарегистрированные ключи доступа на других платформах, см. на соответствующих страницах поддержки этих платформ.
5. Добавить возможность аутентификации с помощью ключа доступа.
Теперь пользователи могут создать и зарегистрировать ключ доступа и готовы использовать его для безопасной аутентификации на вашем сайте. Теперь вам нужно добавить возможность аутентификации с помощью ключа доступа на ваш сайт.
Создайте функцию authenticate()
- В файле
public/client.js
после соответствующего комментария создайте функциюauthenticate()
, которая локально проверяет пользователя, а затем на сервере:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {
// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
};
Получите вызов и другие параметры с конечной точки сервера
Прежде чем попросить пользователя пройти аутентификацию, вам необходимо запросить у сервера параметры для передачи в WebAuthn, включая вызов.
- В теле функции
authenticate()
после соответствующего комментария вызовите функцию_fetch()
для отправкиPOST
запроса на сервер:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');
Сервер этой лабораторной работы предназначен для возврата JSON-данных, максимально похожих на словарь PublicKeyCredentialRequestOptions
, переданный в API WebAuthn navigator.credentials.get()
. Следующий фрагмент кода содержит примеры параметров, которые вы должны получить:
{
"challenge": *****,
"rpId": "passkeys-codelab.glitch.me",
"allowCredentials": []
}
Следующая таблица не является исчерпывающей, но она содержит важные параметры словаря PublicKeyCredentialRequestOptions
:
Параметры | Описания |
Сгенерированный сервером запрос в объекте | |
Идентификатор RP — это домен. Веб-сайт может указать либо свой домен, либо регистрируемый суффикс . Это значение должно совпадать с параметром | |
Это свойство используется для поиска аутентификаторов, подходящих для данной аутентификации. Передайте пустой массив или оставьте его неуказанным, чтобы браузер отображал селектор учётных записей. | |
Установите |
Локально проверьте пользователя и получите учетные данные
- В теле функции
authenticate()
после соответствующего комментария преобразуйте параметрchallenge
обратно в двоичный код:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
- Передайте пустой массив параметру
allowCredentials
, чтобы открыть селектор учетных записей при аутентификации пользователя:
public/client.js
// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];
Селектор учетных записей использует информацию пользователя, сохраненную вместе с ключом доступа.
- Вызовите метод
navigator.credentials.get()
с опциейmediation: 'conditional'
:
public/client.js
// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
publicKey: options,
// Request a conditional UI.
mediation: 'conditional'
});
Эта опция указывает браузеру предлагать пароли в качестве части автозаполнения форм.
Проверьте учетные данные
После того как пользователь локально подтвердит свою личность, вы должны получить объект учетных данных, содержащий подпись, которую вы можете проверить на сервере.
Следующий фрагмент кода включает пример объекта PublicKeyCredential
:
{
"id": *****,
"rawId": *****,
"type": "public-key",
"response": {
"clientDataJSON": *****,
"authenticatorData": *****,
"signature": *****,
"userHandle": *****
},
authenticatorAttachment: "platform"
}
Следующая таблица не является исчерпывающей, но она содержит важные параметры объекта PublicKeyCredential
:
Параметры | Описания |
Идентификатор аутентифицированного ключа доступа, закодированный в формате Base64URL. | |
Версия идентификатора учетных данных объекта | |
Объект | |
Объект | |
Объект | |
Объект | |
Возвращает строку |
Чтобы отправить объект учетных данных на сервер, выполните следующие действия:
- В теле функции
authenticate()
после соответствующего комментария закодируйте двоичные параметры учетных данных, чтобы их можно было доставить на сервер в виде строки:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData = base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);
credential.response = {
clientDataJSON,
authenticatorData,
signature,
userHandle,
};
- Отправьте объект на сервер:
public/client.js
return await _fetch(`/auth/signinResponse`, credential);
При запуске программы сервер возвращает HTTP code 200
, который указывает на то, что учетные данные проверены.
Теперь у вас есть полноценная функция authentication()
!
Проверьте код решения для этого раздела.
public/client.js
// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {
// TODO: Add an ability to authenticate with a passkey: Obtain the
challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');
// TODO: Add an ability to authenticate with a passkey: Locally verify
the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
// The empty allowCredentials array invokes an account selector
by discoverable credentials.
options.allowCredentials = [];
// Invoke the WebAuthn get() function.
const cred = await navigator.credentials.get({
publicKey: options,
// Request a conditional UI.
mediation: 'conditional'
});
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData =
base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);
credential.response = {
clientDataJSON,
authenticatorData,
signature,
userHandle,
};
return await _fetch(`/auth/signinResponse`, credential);
};
6. Добавьте пароли в автозаполнение браузера.
Когда пользователь возвращается, вы хотите, чтобы его вход был максимально простым и безопасным. Если вы добавите кнопку «Войти с паролем» на страницу входа, пользователь сможет нажать её, выбрать пароль в селекторе учётных записей браузера и использовать блокировку экрана для подтверждения личности.
Однако переход с пароля на ключ доступа происходит не для всех пользователей одновременно. Это означает, что вы не сможете избавиться от паролей, пока все пользователи не перейдут на ключи доступа, поэтому до этого момента вам следует оставить форму входа с паролем. Однако, если вы оставите форму ввода пароля и кнопку ввода ключа доступа, пользователям придётся делать ненужный выбор между тем, что использовать для входа. В идеале процесс входа должен быть простым и понятным.
Именно здесь на помощь приходит условный пользовательский интерфейс . Условный пользовательский интерфейс — это функция WebAuthn, позволяющая создать поле ввода формы, которое будет предлагать ключ доступа в качестве элемента автозаполнения в дополнение к паролям. Если пользователь нажмет на ключ доступа в предложениях автозаполнения, ему будет предложено использовать блокировку экрана устройства для локального подтверждения личности. Это обеспечивает бесперебойный пользовательский интерфейс, поскольку действия пользователя практически идентичны действиям при входе с использованием пароля.
Включить условный пользовательский интерфейс
Чтобы включить условный интерфейс, достаточно добавить токен webauthn
в атрибут autocomplete
поля ввода. После установки токена можно вызвать метод navigator.credentials.get()
со строкой mediation: 'conditional'
для активации интерфейса блокировки экрана по условию.
- Чтобы включить условный пользовательский интерфейс, замените существующие поля ввода имени пользователя следующим HTML-кодом после соответствующего комментария в файле
view/index.html
:
view/index.html
<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
type="text"
id="username"
class="mdc-text-field__input"
aria-labelledby="username-label"
name="username"
autocomplete="username webauthn"
autofocus />
Обнаружение функций, вызов WebAuthn и включение условного пользовательского интерфейса
- В файле
view/index.html
после соответствующего комментария замените существующий операторimport
следующим кодом:
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
$,
_fetch,
loading,
authenticate
} from "/client.js";
Этот код импортирует функцию authenticate()
, которую вы реализовали ранее.
- Убедитесь, что объект
window.PulicKeyCredential
доступен и что методPublicKeyCredential.isConditionalMediationAvailable()
возвращаетtrue
значение, а затем вызовите функциюauthenticate()
:
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (
window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable
) {
try {
// Is conditional UI available in this browser?
const cma =
await PublicKeyCredential.isConditionalMediationAvailable();
if (cma) {
// If conditional UI is available, invoke the authenticate() function.
const user = await authenticate();
if (user) {
// Proceed only when authentication succeeds.
$("#username").value = user.username;
loading.start();
location.href = "/home";
} else {
throw new Error("User not found.");
}
}
} catch (e) {
loading.stop();
// A NotAllowedError indicates that the user canceled the operation.
if (e.name !== "NotAllowedError") {
console.error(e);
alert(e.message);
}
}
}
Проверьте код решения для этого раздела.
view/index.html
<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
type="text"
id="username"
class="mdc-text-field__input"
aria-labelledby="username-label"
name="username"
autocomplete="username webauthn"
autofocus
/>
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
$,
_fetch,
loading,
authenticate
} from '/client.js';
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
// Is WebAuthn avaiable in this browser?
if (window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable) {
try {
// Is a conditional UI available in this browser?
const cma= await PublicKeyCredential.isConditionalMediationAvailable();
if (cma) {
// If a conditional UI is available, invoke the authenticate() function.
const user = await authenticate();
if (user) {
// Proceed only when authentication succeeds.
$('#username').value = user.username;
loading.start();
location.href = '/home';
} else {
throw new Error('User not found.');
}
}
} catch (e) {
loading.stop();
// A NotAllowedError indicates that the user canceled the operation.
if (e.name !== 'NotAllowedError') {
console.error(e);
alert(e.message);
}
}
}
Попробуй это
Вы реализовали создание, регистрацию, отображение и аутентификацию паролей на своем сайте.
Чтобы попробовать, выполните следующие действия:
- Перейдите на вкладку предварительного просмотра.
- При необходимости выйдите из системы.
- Щелкните текстовое поле имени пользователя. Появится диалоговое окно.
- Выберите учетную запись, с помощью которой вы хотите войти.
- Подтвердите свою личность с помощью блокировки экрана устройства. Вы будете перенаправлены на страницу
/home
и авторизованы.
7. Поздравляем!
Вы завершили эту практическую работу! Если у вас есть вопросы, задайте их в почтовой рассылке FIDO-DEV или на StackOverflow, используя тег passkey
.