Реализация ключей доступа с автозаполнением форм в веб-приложении

1. Прежде чем начать

Использование паролей вместо ключей доступа — отличный способ сделать учётные записи пользователей на веб-сайтах безопаснее, проще и удобнее. С помощью ключа доступа пользователь может войти на веб-сайт или в приложение, используя функцию блокировки экрана устройства, например, отпечаток пальца, распознавание лица или PIN-код устройства. Ключ доступа необходимо создать, связать с учётной записью пользователя и сохранить его открытый ключ на сервере, прежде чем пользователь сможет войти с его помощью.

В этой лабораторной работе вы превратите базовую форму входа с использованием имени пользователя и пароля в систему, которая поддерживает ключи доступа и включает в себя следующее:

  • Кнопка, которая создает ключ доступа после входа пользователя в систему.
  • Пользовательский интерфейс, отображающий список зарегистрированных ключей доступа.
  • Существующая форма входа, которая позволяет пользователям входить в систему с зарегистрированным паролем через автозаполнение форм.

Предпосылки

Чему вы научитесь

  • Как создать ключ доступа.
  • Как аутентифицировать пользователей с помощью ключа доступа.
  • Как разрешить форме предлагать пароль в качестве варианта входа.

Что вам понадобится

Одна из следующих комбинаций устройств:

  • 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 и развертывать его исключительно из браузера.

Открыть проект

  1. Откройте проект в Glitch .
  2. Нажмите Remix , чтобы создать ответвление проекта Glitch.
  3. В навигационном меню в нижней части Glitch нажмите «Предварительный просмотр» > «Предварительный просмотр в новом окне» . В браузере откроется ещё одна вкладка.

Кнопка «Просмотр в новом окне» в навигационном меню внизу Glitch

Проверьте начальное состояние веб-сайта

  1. На вкладке предварительного просмотра введите случайное имя пользователя и нажмите кнопку Далее .
  2. Введите случайный пароль и нажмите «Войти» . Пароль будет проигнорирован, но вы всё равно пройдете аутентификацию и окажетесь на главной странице.
  3. Если вы хотите изменить отображаемое имя, сделайте это. Это всё, что вы можете сделать в исходном состоянии.
  4. Нажмите Выйти .

В этом состоянии пользователи должны вводить пароль при каждом входе в систему. Вы добавляете поддержку ключа доступа в эту форму, чтобы пользователи могли входить в систему, используя функцию блокировки экрана устройства. Вы можете попробовать конечное состояние по адресу https://passkeys-codelab.glitch.me/ .

Дополнительную информацию о работе паролей см. в разделе Как работают пароли?.

3. Добавить возможность создания ключа доступа.

Чтобы разрешить пользователям проходить аутентификацию с помощью ключа доступа, необходимо предоставить им возможность создавать и регистрировать ключ доступа, а также хранить его открытый ключ на сервере.

При создании ключа доступа появляется диалоговое окно проверки пользователя ключа доступа.

Вы хотите разрешить создание ключа доступа после входа пользователя с паролем и добавить пользовательский интерфейс, позволяющий пользователям создавать ключ доступа и просматривать список всех зарегистрированных ключей доступа на странице /home . В следующем разделе вы создадите функцию, которая создаёт и регистрирует ключ доступа.

Создайте функцию registerCredential()

  1. В Glitch перейдите к файлу public/client.js и прокрутите его до конца.
  2. После соответствующего комментария добавьте следующую функцию 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 :

Параметры

Описания

challenge

Сгенерированный сервером запрос в объекте ArrayBuffer для этой регистрации. Он обязателен, но не используется во время регистрации, за исключением случаев аттестации — это сложная тема, которая не рассматривается в этой практической работе.

user.id

Уникальный идентификатор пользователя. Это значение должно быть объектом ArrayBuffer , не содержащим персональные данные, такие как адреса электронной почты или имена пользователей. Подойдёт случайное 16-байтовое значение, генерируемое для каждой учётной записи.

user.name

Это поле должно содержать уникальный идентификатор учётной записи, узнаваемый пользователем, например, адрес электронной почты или имя пользователя. Он отображается в селекторе учётных записей. (Если вы используете имя пользователя, используйте то же значение, что и при аутентификации по паролю.)

user.displayName

Это поле — необязательное, удобное для пользователя имя учётной записи. Оно не обязательно должно быть уникальным и может быть выбранным пользователем. Если на вашем сайте нет подходящего значения для этого поля, передайте пустую строку. В зависимости от браузера, это имя может отображаться в селекторе учётных записей.

rp.id

Идентификатор проверяющей стороны (RP) — это домен. Веб-сайт может указать либо свой домен, либо регистрируемый суффикс . Например, если происхождение RP — https://login.example.com:1337, идентификатор RP может быть login.example.com или example.com . Если идентификатор RP указан как example.com , пользователь может пройти аутентификацию на login.example.com или на любом другом поддомене example.com.

pubKeyCredParams

В этом поле указываются поддерживаемые RP алгоритмы с открытым ключом. Мы рекомендуем установить его в значение [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}] . Это включает поддержку ECDSA с P-256 и RSA PKCS#1 , обеспечивая полное покрытие.

excludeCredentials

Предоставляет список уже зарегистрированных идентификаторов учётных данных, чтобы предотвратить двойную регистрацию одного и того же устройства. При наличии, элемент transports должен содержать результат вызова функции getTransports() при регистрации каждого учётного данных.

authenticatorSelection.authenticatorAttachment

Установите значение "platform" . Это означает, что вам нужен аутентификатор, встроенный в устройство платформы, чтобы пользователю не приходилось вставлять что-либо, например USB-ключ безопасности.

authenticatorSelection.requireResidentKey

Установите логическое значение true . Обнаруживаемые учётные данные (резидентный ключ) можно использовать без необходимости предоставления сервером идентификатора учётных данных, поэтому они совместимы с автозаполнением.

authenticatorSelection.userVerification

Установите "preferred" значение или опустите его, так как это значение по умолчанию. Это указывает, является ли верификация пользователя с использованием блокировки экрана устройства "required" , "preferred" или "discouraged" . Установка "preferred" значения запрашивает верификацию пользователя, когда устройство поддерживает эту функцию.

Создать учетные данные

  1. В теле функции 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);
  }
}
  1. В следующей строке установите для authenticatorSelection.authenticatorAttachment значение "platform" , а authenticatorSelection.requireResidentKey — значение true . Это позволит использовать только платформенный аутентификатор (само устройство) с возможностью обнаружения учётных данных.

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. На следующей строке вызовите метод 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 :

Параметры

Описания

id

Идентификатор созданного ключа доступа, закодированный в формате Base64URL. Этот идентификатор помогает браузеру определить, есть ли соответствующий ключ доступа на устройстве при аутентификации. Это значение должно храниться в базе данных на сервере.

rawId

Версия идентификатора учетных данных объекта ArrayBuffer .

response.clientDataJSON

Объект ArrayBuffer закодировал клиентские данные.

response.attestationObject

Объект аттестации, закодированный в ArrayBuffer . Он содержит важную информацию, такую как идентификатор RP, флаги и открытый ключ.

response.transports

Список транспортов, поддерживаемых устройством: "internal" означает, что устройство поддерживает ключ доступа. "hybrid" означает, что оно также поддерживает аутентификацию на другом устройстве .

authenticatorAttachment

Возвращает "platform" , если эти учетные данные созданы на устройстве с поддержкой ключа доступа.

Чтобы отправить объект учетных данных на сервер, выполните следующие действия:

  1. Закодируйте двоичные параметры учетных данных как 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
};
  1. На следующей строке отправьте объект на сервер:

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() доступна, вам нужна кнопка для её вызова. Кроме того, вам нужно отобразить список зарегистрированных паролей.

Зарегистрированные ключи доступа, перечисленные на странице /home

Добавить HTML-заполнитель

  1. В Glitch перейдите к файлу views/home.html .
  2. После соответствующего комментария добавьте заполнитель пользовательского интерфейса, который отображает кнопку для регистрации ключа доступа и список ключей доступа:

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 класс, чтобы кнопка «Создать ключ доступа» отображалась.

Чтобы проверить, поддерживает ли среда ключи доступа, выполните следующие действия:

  1. В конце файла 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) {
  1. В теле условного выражения проверьте, может ли устройство создать ключ доступа, а затем проверьте, может ли ключ доступа быть предложен при автозаполнении формы.

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()
  ]);
  1. Если все условия выполнены, показать кнопку для создания ключа доступа. В противном случае вывести предупреждение.

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.';
}

Отобразить зарегистрированные ключи доступа в списке

  1. Определите функцию 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);
};
  1. На следующей строке вызовите функцию renderCredentials() для отображения зарегистрированных ключей доступа, как только пользователь перейдет на страницу /home в качестве инициализации.

views/home.html

renderCredentials();

Создайте и зарегистрируйте ключ доступа

Чтобы создать и зарегистрировать ключ доступа, вам необходимо вызвать функцию registerCredential() , которую вы реализовали ранее.

Чтобы вызвать функцию registerCredential() при нажатии кнопки «Создать ключ доступа» , выполните следующие действия:

  1. В файле после HTML-заполнителя найдите следующий оператор import :

views/home.html

import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
} from '/client.js';
  1. В конце тела оператора 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';
  1. В конце файла, после соответствующего комментария, определите функцию 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();
  1. В теле функции 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);
    }
  }
};
  1. В строке после функции 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);

Попробуй это

Если вы выполнили все шаги до сих пор, вы реализовали возможность создания, регистрации и отображения паролей на веб-сайте!

Чтобы попробовать, выполните следующие действия:

  1. На вкладке предварительного просмотра войдите в систему, используя случайное имя пользователя и пароль.
  2. Нажмите Создать ключ доступа .
  3. Подтвердите свою личность с помощью блокировки экрана устройства.
  4. Подтвердите, что ключ доступа зарегистрирован и отображается в разделе «Ваши зарегистрированные ключи доступа» на веб-странице.

Зарегистрированные ключи доступа указаны на странице /home.

Переименовать и удалить зарегистрированные ключи доступа

Вы должны иметь возможность переименовывать или удалять зарегистрированные пароли в списке. Вы можете проверить, как это работает, в коде, который прилагается к практической работе.

В 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 :

Параметры

Описания

challenge

Сгенерированный сервером запрос в объекте ArrayBuffer . Это необходимо для предотвращения атак с повторным воспроизведением. Никогда не принимайте один и тот же запрос в ответе дважды. Считайте это CSRF-токеном .

rpId

Идентификатор RP — это домен. Веб-сайт может указать либо свой домен, либо регистрируемый суффикс . Это значение должно совпадать с параметром rp.id , использованным при создании ключа доступа.

allowCredentials

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

userVerification

Установите "preferred" значение или опустите его, так как это значение по умолчанию. Это указывает, является ли верификация пользователя с помощью блокировки экрана устройства "required" , "preferred" или "discouraged" . Установка "preferred" значения запрашивает верификацию пользователя, когда устройство поддерживает её.

Локально проверьте пользователя и получите учетные данные

  1. В теле функции 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);
  1. Передайте пустой массив параметру allowCredentials , чтобы открыть селектор учетных записей при аутентификации пользователя:

public/client.js

// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];

Селектор учетных записей использует информацию пользователя, сохраненную вместе с ключом доступа.

  1. Вызовите метод 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 :

Параметры

Описания

id

Идентификатор аутентифицированного ключа доступа, закодированный в формате Base64URL.

rawId

Версия идентификатора учетных данных объекта ArrayBuffer .

response.clientDataJSON

Объект ArrayBuffer клиентских данных. Это поле содержит информацию, такую как запрос и источник, которую RP-сервер должен проверить.

response.authenticatorData

Объект ArrayBuffer с данными аутентификатора. Это поле содержит информацию, например, идентификатор RP.

response.signature

Объект ArrayBuffer подписи. Это значение является основой учётных данных и должно быть проверено на сервере.

response.userHandle

Объект ArrayBuffer , содержащий идентификатор пользователя, заданный при создании. Это значение можно использовать вместо идентификатора учётных данных, если серверу необходимо выбрать используемые значения идентификаторов или если бэкенд хочет избежать создания индекса по идентификаторам учётных данных.

authenticatorAttachment

Возвращает строку "platform" , если эти учётные данные получены с локального устройства. В противном случае возвращает строку "cross-platform" , особенно если пользователь использует телефон для входа . Если пользователю требуется использовать телефон для входа, предложите ему создать ключ доступа на локальном устройстве.

Чтобы отправить объект учетных данных на сервер, выполните следующие действия:

  1. В теле функции 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,
};
  1. Отправьте объект на сервер:

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 и включение условного пользовательского интерфейса

  1. В файле 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() , которую вы реализовали ранее.

  1. Убедитесь, что объект 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);
    }
  }
}

Попробуй это

Вы реализовали создание, регистрацию, отображение и аутентификацию паролей на своем сайте.

Чтобы попробовать, выполните следующие действия:

  1. Перейдите на вкладку предварительного просмотра.
  2. При необходимости выйдите из системы.
  3. Щелкните текстовое поле имени пользователя. Появится диалоговое окно.
  4. Выберите учетную запись, с помощью которой вы хотите войти.
  5. Подтвердите свою личность с помощью блокировки экрана устройства. Вы будете перенаправлены на страницу /home и авторизованы.

Диалоговое окно, предлагающее вам подтвердить свою личность с помощью сохраненного пароля или ключа доступа.

7. Поздравляем!

Вы завершили эту практическую работу! Если у вас есть вопросы, задайте их в почтовой рассылке FIDO-DEV или на StackOverflow, используя тег passkey .

Узнать больше