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

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

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

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

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

Предварительные требования

Что вы узнаете

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

2. Настройка

В этом практическом задании вы клонируете незавершенное демонстрационное приложение с GitHub, а затем завершите реализацию поддержки паролей.

Клонируйте проект

  1. Откройте проект на GitHub .
  2. Клонируйте или скачайте проект.

ac587c53b746785a.png

Запустите проект

  1. Откройте терминал и перейдите в каталог cd start , чтобы сменить директорию.
  2. Для установки зависимостей проекта выполните npm install .
  3. Соберите и запустите проект с помощью npm run build && IS_LOCAL=1 npm run start .
  4. Откройте http://localhost:8080/ в своем браузере.

Проверьте исходное состояние веб-сайта.

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

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

Для получения более подробной информации о том, как работают пароли, см. раздел «Как работают пароли?» .

3. Добавить возможность создания пароля.

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

9b84dbaec66afe9c.png

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

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

  1. В выбранном вами редакторе кода откройте start директорию.
  2. Перейдите к файлу public/client.js и прокрутите страницу до конца.
  3. После соответствующего комментария добавьте следующую функцию 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 ID) — это домен. Веб-сайт может указать либо свой домен, либо регистрируемый суффикс . Например, если источником RP является https://login.example.com:1337, то RP ID может быть либо login.example.com , либо example.com . Если RP ID указан как 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 . Это можно сделать с помощью функции PublicKeyCredential.parseCreationOptionsFromJSON() :

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.

// Deserialize and decode the `PublicKeyCredential.parseCreationOptionsFromJSON()`.
const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);
  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 ID), флаги и открытый ключ.

response.transports

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

authenticatorAttachment

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

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

  1. Закодируйте двоичные параметры учетных данных в формате Base64URL, чтобы они могли быть переданы на сервер в виде строки. Для этого можно использовать .toJSON() :

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

// Encode and serialize the `PublicKeyCredential`.
const credential = JSON.stringify(cred);
  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 the server endpoint.

  const _options = await _fetch('/auth/registerRequest');

  // TODO: Add an ability to create a passkey: Create a credential.

  // Deserialize and decode the `PublicKeyCredential.parseCreationOptionsFromJSON()`.
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);

  // 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.

  // Encode and serialize the `PublicKeyCredential`.
  const credential = JSON.stringify(cred);

  return await _fetch('/auth/registerResponse', credential);
};

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

Теперь, когда функция registerCredential() доступна, вам нужна кнопка для её вызова. Кроме того, вам необходимо отобразить список зарегистрированных паролей.

bfa4e7cdda47669e.png

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

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

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3>Your registered passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mdui-button id="create-passkey" class="hidden" icon="fingerprint" type="button">Create a passkey</mdui-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 capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
        capabilities.passkeyPlatformAuthenticator === true) {
  1. Если все условия выполнены, отобразите кнопку для создания пароля. В противном случае отобразите предупреждающее сообщение.

views/home.html

      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 = res.length > 0 ? html`
    <mdui-list>
      ${res.map(cred => html`
        <mdui-list-item>
          ${cred.name || 'Unnamed'}
          <mdui-button-icon data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed'}" @click="${rename}" icon="edit" slot="end-icon"></mdui-button-icon>
          <mdui-button-icon data-cred-id="${cred.id}" @click="${remove}" icon="delete" slot="end-icon"></mdui-button-icon>
        </mdui-list-item>`)}
    </mdui-list>` : html`
    <mdui-list>
      <mdui-list-item>No credentials found.</mdui-list-item>
    </mdui-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>Your registered passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mdui-button id="create-passkey" icon="fingerprint" type="button">Create a passkey</mdui-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');

// Is WebAuthn available in this browser?
if (window.PublicKeyCredential &&
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
  PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
      capabilities.passkeyPlatformAuthenticator === 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`
    <mdui-list>
      ${res.map(cred => html`
        <mdui-list-item>
          ${cred.name || 'Unnamed'}
          <mdui-button-icon data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed'}" @click="${rename}" icon="edit" slot="end-icon"></mdui-button-icon>
          <mdui-button-icon data-cred-id="${cred.id}" @click="${remove}" icon="delete" slot="end-icon"></mdui-button-icon>
        </mdui-list-item>`)}
    </mdui-list>` : html`
    <mdui-list>
      <mdui-list-item>No credentials found.</mdui-list-item>
    </mdui-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 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).

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

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

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

// Base64URL decode the challenge.
const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);

Сервер в этом практическом задании предназначен для возврата JSON, максимально похожего на словарь PublicKeyCredentialRequestOptions , передаваемый в API WebAuthn navigator.credentials.get() . Следующий фрагмент кода содержит пример параметров, которые вы должны получить:

{
  "challenge": *****,
  "rpId": "localhost",
  "allowCredentials": []
}

Приведенная ниже таблица не является исчерпывающей, но содержит важные параметры из словаря PublicKeyCredentialRequestOptions :

Параметры

Описания

challenge

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

rpId

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

allowCredentials

Это свойство используется для поиска аутентификаторов, подходящих для данной аутентификации. Передайте пустой массив или оставьте его неуказанным, чтобы браузер отобразил селектор учетной записи. Подробнее о работе свойства 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() после соответствующего комментария закодируйте двоичные параметры учетных данных, чтобы их можно было передать на сервер в виде строки. Для этого можно использовать .toJSON() :

public/client.js

// TODO: Add an ability to authenticate with a passkey: Verify the credential.
// Encode and serialize the `PublicKeyCredential`.
const credential = JSON.stringify(cred);
  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, позволяющая создать поле ввода формы, которое будет предлагать пароль в качестве одного из вариантов автозаполнения в дополнение к паролям. Если пользователь нажимает на пароль в предложенных вариантах автозаполнения, ему предлагается использовать блокировку экрана устройства для локальной проверки личности. Это обеспечивает удобный пользовательский интерфейс, поскольку действия пользователя практически идентичны входу в систему с помощью пароля.

d616744939063451.png

Включите условный пользовательский интерфейс.

Чтобы включить условное отображение пользовательского интерфейса, достаточно добавить токен webauthn в атрибут autocomplete поля ввода. Установив токен, вы можете вызвать метод navigator.credentials.get() со строкой mediation: 'conditional' чтобы условно активировать блокировку экрана.

  • Чтобы включить условный интерфейс, замените существующие поля ввода имени пользователя следующим HTML-кодом после соответствующего комментария в файле view/index.html :

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<mdui-text-field id="username" label="Username" name="username" autocomplete="username webauthn" autofocus></mdui-text-field>

Обнаружение функций, вызов 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.getClientCapabilities) {
  try {

    // Is conditional UI available in this browser?
      const capabilities = await PublicKeyCredential.getClientCapabilities();
      if (capabilities.conditionalGet) {

      // 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. -->
<mdui-text-field id="username" label="Username" name="username" autocomplete="username webauthn" autofocus></mdui-text-field>

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 available on this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.getClientCapabilities) {
  try {
    // Is conditional UI available in this browser?
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    if (capabilities.conditionalGet) {
      // 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);
    }
  }
}

Попробуйте!

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

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

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

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

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

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

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