Защитите свой сайт с помощью двухфакторной аутентификации с помощью ключа безопасности (WebAuthn).

1. Что вы будете строить

Вы начнете с базового веб-приложения, которое поддерживает вход на основе пароля.

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

  • Способ для пользователя зарегистрировать учетные данные WebAuthn.
  • Поток двухфакторной аутентификации, при котором пользователю предлагается указать второй фактор — учетные данные WebAuthn, если они зарегистрированы.
  • Интерфейс управления учетными данными: список учетных данных, который позволяет пользователям переименовывать и удалять учетные данные.

16ce77744061c5f7.png

Взгляните на готовое веб-приложение и попробуйте его.

2. О WebAuthn

Основы WebAuthn

Почему WebAuthn?

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

Web Authentication API, или WebAuthn , — это стандартизированный защищенный от фишинга протокол, который может использоваться любым веб-приложением.

Как это работает

Источник: webauthn.guide

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

  • Закрытый ключ надежно хранится на устройстве пользователя.
  • Открытый ключ и случайно сгенерированный идентификатор учетных данных отправляются на сервер для хранения.

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

Преимущества

WebAuthn имеет два основных преимущества:

  • Нет общего секрета: сервер не хранит секрета. Это делает базы данных менее привлекательными для хакеров, поскольку открытые ключи для них бесполезны.
  • Учетные данные с заданной областью действия: учетные данные, зарегистрированные для site.example , нельзя использовать на evil-site.example . Это делает WebAuthn защищенным от фишинга.

Сценарии использования

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

Поддержка браузера

Он написан W3C и FIDO при участии Google, Mozilla, Microsoft, Yubico и других.

Глоссарий

  • Аутентификатор: программный или аппаратный объект, который может зарегистрировать пользователя, а затем заявить о владении зарегистрированными учетными данными. Существует два типа аутентификаторов:
  • Мобильный аутентификатор: аутентификатор, который можно использовать с любым устройством, с которого пользователь пытается войти. Пример: USB-ключ безопасности, смартфон.
  • Аутентификатор платформы: аутентификатор, встроенный в устройство пользователя. Пример: Apple Touch ID.
  • Учетные данные: пара закрытый-открытый ключ
  • Проверяющая сторона: (сервер) веб-сайта, который пытается аутентифицировать пользователя.
  • Сервер FIDO: сервер, который используется для аутентификации. FIDO — это семейство протоколов, разработанных альянсом FIDO; одним из этих протоколов является WebAuthn.

В этом семинаре мы будем использовать перемещаемый аутентификатор.

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

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

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

  • Базовое понимание WebAuthn.
  • Базовые знания JavaScript и HTML.
  • Современный браузер с поддержкой WebAuthn.
  • Ключ безопасности , совместимый с U2F .

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

  • Телефон Android с Android >= 7 (Nougat), на котором работает Chrome. В этом случае вам также понадобится компьютер с Windows, macOS или Chrome OS с работающим Bluetooth.
  • USB-ключ, например YubiKey .

6539dc7ffec2538c.png

Источник: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

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

Вы научитесь ✅

  • Как зарегистрировать и использовать ключ безопасности в качестве второго фактора аутентификации WebAuthn.
  • Как сделать этот процесс удобным.

Ты не научишься ❌

  • Как построить сервер FIDO — сервер, который используется для аутентификации. Это нормально, потому что, как правило, как разработчик веб-приложений или сайтов, вы полагаетесь на существующие реализации сервера FIDO. Всегда проверяйте функциональность и качество серверных реализаций, на которые вы полагаетесь. В этой лаборатории кода сервер FIDO использует SimpleWebAuthn . Другие варианты смотрите на официальной странице FIDO Alliance . Библиотеки с открытым исходным кодом см. на webauthn.io или AwesomeWebAuthn .

Отказ от ответственности

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

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

4. Настройте свой аутентификатор

Если вы используете телефон Android в качестве аутентификатора

  • Убедитесь, что Chrome обновлен как на компьютере, так и на телефоне.
  • На компьютере и телефоне откройте Chrome и войдите в систему с тем же профилем, который вы хотите использовать для этого семинара.
  • Включите синхронизацию для этого профиля на компьютере и телефоне . Используйте для этого chrome://settings/syncSetup.
  • Включите Bluetooth как на рабочем столе, так и на телефоне.
  • На рабочем столе Chrome, войдя в систему с тем же профилем, откройте webauthn.io .
  • Введите простое имя пользователя. Оставьте для Тип аттестации и Тип аутентификатора значения Нет и Не указано (по умолчанию). Щелкните Зарегистрироваться .

6b49ff0298f5a0af.png

  • Должно открыться окно браузера с просьбой подтвердить вашу личность. Выберите свой телефон в списке.

ffebe58ac826eaf2.png852de328fcd4eb42.png

  • На вашем телефоне должно появиться уведомление под названием « Подтвердите свою личность ». Коснитесь его.
  • На вашем телефоне вас попросят ввести PIN-код вашего телефона (или прикоснуться к датчику отпечатков пальцев). Введите его.
  • На webauthn.io на рабочем столе должен появиться индикатор «Успех».

fc0acf00a4d412fa.png

  • На веб-сайте webauthn.io на рабочем столе нажмите кнопку «Войти».
  • Снова должно открыться окно браузера; выберите свой телефон в списке.
  • На телефоне коснитесь всплывающего уведомления и введите свой PIN-код (или коснитесь датчика отпечатков пальцев).
  • webauthn.io должен сообщить вам, что вы вошли в систему. Ваш телефон работает должным образом в качестве ключа безопасности; все готово для семинара!

Если вы используете USB-ключ безопасности в качестве аутентификатора

  • На рабочем столе Chrome откройте webauthn.io .
  • Введите простое имя пользователя. Оставьте для Тип аттестации и Тип аутентификатора значения Нет и Не указано (по умолчанию). Щелкните Зарегистрироваться .
  • Должно открыться окно браузера с просьбой подтвердить вашу личность. Выберите в списке ключ безопасности USB .

ffebe58ac826eaf2.png9fe75f04e43da035.png

  • Вставьте ключ безопасности на рабочий стол и коснитесь его.

923d5adb8aa8286c.png

  • На webauthn.io на рабочем столе должен появиться индикатор «Успех».

fc0acf00a4d412fa.png

  • На веб-сайте webauthn.io на рабочем столе нажмите кнопку « Войти ».
  • Снова должно открыться окно браузера; выберите в списке ключ безопасности USB .
  • Коснитесь ключа.
  • Webauthn.io должен сообщить вам, что вы вошли в систему. Ваш USB-ключ безопасности работает правильно; все готово для семинара!

7e1c0bb19c9f3043.png

5. Настройте

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

Разветвить стартовый код

Откройте стартовый проект .

Нажмите кнопку Ремикс .

Это создает копию стартового кода. Теперь у вас есть собственный код для редактирования. Ваш форк (называемый «remix» в Glitch) — это место, где вы будете выполнять всю работу для этой кодлабы.

cf2b9f552c9809b6.png

Исследуйте стартовый код

Немного изучите начальный код, который вы только что разветвили.

Обратите внимание, что в разделе libs уже имеется библиотека с именем auth.js Это настраиваемая библиотека, которая отвечает за логику аутентификации на стороне сервера. Он использует библиотеку fido в качестве зависимости.

6. Реализуйте регистрацию учетных данных

Реализовать регистрацию учетных данных

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

Давайте сначала добавим функцию, которая делает это в нашем клиентском коде.

Обратите внимание, что в public/auth.client.js есть функция registerCredential() , которая пока ничего не делает. Добавьте в него следующий код:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

Обратите внимание, что эта функция уже экспортирована для вас.

Вот что делает registerCredential :

  • Он получает параметры создания учетных данных с сервера ( /auth/credential-options ).
  • Поскольку параметры сервера возвращаются закодированными, для их декодирования используется служебная функция decodeServerOptions .
  • Он создает учетные данные, вызывая веб-API navigator.credential.create . Когда вызывается navigator.credential.create , браузер вступает во владение и предлагает пользователю выбрать ключ безопасности.
  • Он декодирует вновь созданные учетные данные
  • Он регистрирует новые учетные данные на стороне сервера, отправляя запрос к /auth/credential , содержащему закодированные учетные данные.

В сторону: взгляните на код сервера

registerCredential() делает два обращения к серверу, так что давайте посмотрим, что происходит в бэкенде.

Варианты создания учетных данных

Когда клиент отправляет запрос ( /auth/credential-options ), сервер создает объект параметров и отправляет его обратно клиенту.

Затем этот объект используется клиентом в фактическом вызове создания учетных данных:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

Итак, что в этом credentialCreationOptions используется в клиентском registerCredential , который вы реализовали на предыдущем шаге?

Взгляните на код сервера в router.post("/credential-options", ... .

Давайте не будем рассматривать каждое отдельное свойство, но вот несколько интересных, которые вы можете увидеть в объекте options кода сервера , который сгенерирован с использованием библиотеки fido2 и в конечном итоге возвращен клиенту:

  • rpName и rpId описывают организацию, которая регистрирует и аутентифицирует пользователя. Помните, что в WebAuthn учетные данные привязаны к определенному домену, что является преимуществом безопасности; rpName и rpId здесь используются для определения области учетных данных. Действительный rpId — это, например, имя хоста вашего сайта. Обратите внимание, как они будут автоматически обновляться, когда вы разветвляете стартовый проект 🧘🏻‍♀️
  • excludeCredentials — список учетных данных; новые учетные данные не могут быть созданы на аутентификаторе, который также содержит один из учетных данных, перечисленных в excludeCredentials . В нашей лаборатории кода excludeCredentials — это список существующих учетных данных для этого пользователя. С этим и user.id мы гарантируем, что все учетные данные, созданные пользователем, будут жить на другом аутентификаторе (ключе безопасности). Это хорошая практика, поскольку это означает, что если пользователь зарегистрировал несколько учетных данных, они будут использовать разные аутентификаторы (ключи безопасности), поэтому потеря одного ключа безопасности не приведет к блокировке учетной записи пользователя.
  • authenticatorSelection определяет тип аутентификаторов, которые вы хотите разрешить в своем веб-приложении. Давайте поближе взглянем на authenticatorSelection :
    • residentKey: preferred означает, что это приложение не применяет обнаруживаемые учетные данные на стороне клиента. Учетные данные, обнаруживаемые на стороне клиента, — это особый тип учетных данных, который позволяет аутентифицировать пользователя без необходимости его предварительной идентификации. Здесь мы установили preferred , потому что эта лаборатория кода фокусируется на базовой реализации; обнаруживаемые учетные данные предназначены для более сложных потоков.
    • requireResidentKey присутствует только для обратной совместимости с WebAuthn v1.
    • userVerification: preferred означает, что если средство проверки подлинности поддерживает проверку пользователя (например, если это биометрический ключ безопасности или ключ со встроенной функцией PIN-кода), проверяющая сторона будет запрашивать его при создании учетных данных. Если аутентификатор не является базовым ключом безопасности, сервер не будет запрашивать проверку пользователя.
  • ​​pubKeyCredParam описывает в порядке предпочтения желаемые криптографические свойства учетных данных.

Все эти варианты являются решениями, которые веб-приложение должно принять для своей модели безопасности. Обратите внимание, что на сервере эти параметры определены в одном объекте authSettings .

Вызов

Еще один интересный момент: req.session.challenge = options.challenge; .

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

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

Код регистрации учетных данных

Взгляните на код сервера в router.post("/credential", ... .

Здесь учетные данные регистрируются на стороне сервера.

Итак, что там происходит?

Один из наиболее примечательных фрагментов этого кода — вызов проверки через fido2.verifyAttestationResponse :

  • Подписанный вызов проверяется, и это гарантирует, что учетные данные были созданы кем-то, кто фактически задержал закрытый ключ во время создания.
  • Также проверяется идентификатор проверяющей стороны, привязанный к ее источнику. Это гарантирует, что учетные данные привязаны к этому веб-приложению (и только к этому веб-приложению).

Добавьте эту функцию в пользовательский интерфейс

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

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

В разметке account.html под именем пользователя есть пустой div с классом макета class="flex-h-between" . Мы будем использовать этот div для элементов пользовательского интерфейса, связанных с функциональностью 2FA.

Добавьте в этот div:

  • Заголовок с надписью «Двухфакторная аутентификация».
  • Кнопка для создания учетных данных
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

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

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

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

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

Отображение учетных данных для просмотра пользователем

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

Страница учетной записи является хорошим местом для этого.

В account.html найдите функцию с именем updateCredentialList() .

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

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}    

Пока не возражайте против removeEl и renameEl ; вы узнаете о них позже в этой кодовой лаборатории.

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

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

Теперь вызовите updateCredentialList после успешного завершения registerCredential , чтобы в списках отображались вновь созданные учетные данные:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Попробуйте! 👩🏻‍💻

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

Попробуйте:

  • Выход.
  • Войти — под любым пользователем и паролем. Как упоминалось ранее, пароль на самом деле не проверяется на правильность, чтобы не усложнять эту лабораторию кода. Введите любой непустой пароль.
  • Когда вы окажетесь на странице « Учетная запись », нажмите «Добавить учетные данные ».
  • Вам будет предложено вставить и коснуться ключа безопасности. Сделай это.
  • После успешного создания учетных данных учетные данные должны отображаться на странице учетной записи.
  • Перезагрузите страницу учетной записи . Учетные данные должны отображаться.
  • Если у вас есть два доступных ключа, попробуйте добавить два разных ключа безопасности в качестве учетных данных. Они оба должны отображаться.
  • Попробуйте создать два учетных данных с одним и тем же аутентификатором (ключом); вы заметите, что это не будет поддерживаться. Это сделано намеренно — это связано с тем, что мы используем excludeCredentials в бэкенде.

7. Включите двухфакторную аутентификацию

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

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

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

6ff49a7e520836d0.png

К этому двухфакторному потоку:

e7409946cd88efc7.png

Реализовать двухфакторную аутентификацию

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

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

В public/auth.client.js найдите пустую функцию authenticateTwoFactor и добавьте к ней следующий код:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

Обратите внимание, что эта функция уже экспортирована для вас; он понадобится нам на следующем шаге.

Вот что делает authenticateTwoFactor :

  • Он запрашивает два варианта аутентификации с сервера. Как и параметры создания учетных данных, которые вы видели ранее, они определяются на сервере и зависят от модели безопасности веб-приложения. Покопайтесь в коде сервера в router.post("/two-factors-options", ... для деталей.
  • Вызывая navigator.credentials.get , он позволяет браузеру взять на себя управление и предложить пользователю вставить и коснуться ранее зарегистрированного ключа. Это приводит к выбору учетных данных для этой конкретной операции проверки подлинности второго фактора.
  • Затем выбранные учетные данные передаются в запросе бэкэнда на fetch("/auth/authenticate-two-factor"`. Если учетные данные действительны для этого пользователя, пользователь проходит аутентификацию.

В сторону: взгляните на код сервера

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

Теперь взгляните на код сервера в router.post("/initialize-authentication", ... .

Здесь следует отметить два интересных момента:

  • На этом этапе одновременно проверяются и пароль, и учетные данные. Это мера безопасности: для пользователей, у которых настроена двухфакторная аутентификация, мы не хотим, чтобы потоки пользовательского интерфейса выглядели по-разному в зависимости от того, был ли введен правильный пароль. Таким образом, на этом шаге мы одновременно проверяем и пароль, и учетные данные.
  • Если и пароль, и учетные данные действительны, мы завершаем аутентификацию, вызывая completeAuthentication(req, res); На практике это означает, что мы переключаем сеансы с временного сеанса auth , где пользователь еще не аутентифицирован, на основной сеанс main , где пользователь аутентифицирован.

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

В папке views обратите внимание на новую страницу second-factor.html .

На нем есть кнопка с надписью « Использовать ключ безопасности », но пока она ничего не делает.

Сделайте так, чтобы эта кнопка вызывала authenticateTwoFactor() при нажатии.

  • Если authenticateTwoFactor() выполнена успешно, пользователь перенаправляется на страницу своей учетной записи .
  • Если это не удалось, предупредите пользователя о том, что произошла ошибка. В реальном приложении вы бы реализовали более полезные сообщения об ошибках — для простоты в этой демонстрации мы будем использовать только предупреждение окна.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

Используйте двухфакторную аутентификацию

Теперь все готово для добавления шага двухфакторной аутентификации.

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

322a5c49d865a0d8.png

В index.html ниже location.href = "/account"; , добавьте код, который условно перенаправляет пользователя на страницу второй факторной аутентификации, если он настроил двухфакторную аутентификацию.

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

Обратите внимание, что server.js также реализует проверку сеанса на стороне сервера, что гарантирует, что только авторизованные пользователи могут получить доступ к account.html .

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

Попробуйте! 👩🏻‍💻

  • Войдите под новым пользователем johndoe .
  • Выйти.
  • Войдите в свою учетную запись как johndoe ; см., что требуется только пароль.
  • Создайте учетную запись. Это фактически будет означать, что вы активировали двухфакторную аутентификацию как johndoe .
  • Выйти.
  • Введите имя пользователя johndoe и пароль.
  • Посмотрите, как вы автоматически переходите на страницу двухфакторной аутентификации.
  • (Попробуйте получить доступ к странице учетной записи в /account ; обратите внимание, как вы перенаправлены на страницу индекса, потому что вы не полностью аутентифицированы: вам не хватает второго фактора)
  • Вернитесь на страницу двухфакторной аутентификации и нажмите Использовать ключ безопасности для двухфакторной аутентификации.
  • Теперь вы вошли в систему и должны увидеть страницу своей учетной записи !

8. Упростите использование учетных данных

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

Но... Вы заметили?

На данный момент наш список учетных данных не очень удобен: идентификатор учетных данных и открытый ключ представляют собой длинные строки, которые бесполезны при управлении учетными данными! Люди не слишком хорошо разбираются в длинных строках и числах 🤖

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

Взгляните на renameCredential

Чтобы сэкономить ваше время на реализацию этой функции, которая не делает ничего особенно революционного, в начальный код в auth.client.js была добавлена ​​функция для переименования учетных данных:

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

Это обычный вызов обновления базы данных: клиент отправляет запрос PUT на серверную часть с идентификатором учетных данных и новым именем для этих учетных данных.

Внедрение пользовательских имен учетных данных

Обратите внимание на пустую функцию rename в account.html .

Добавьте к нему следующий код::

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

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

Используйте функцию rename в register() , чтобы пользователи могли называть учетные данные при регистрации:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Обратите внимание, что пользовательский ввод будет проверяться и очищаться в бэкэнде:

  check("name")
    .trim()
    .escape()

Отображение имен учетных данных

Перейдите к getCredentialHtml в templates.js .

Обратите внимание, что уже есть код для отображения имени учетных данных в верхней части карточки учетных данных:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

Попробуйте! 👩🏻‍💻

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

Включить переименование учетных данных

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

В account.html ищем пока пустую функцию renameEl и добавляем к ней следующий код:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

Теперь в getCredentialHtml templates.js в div class="flex-end" добавьте следующий код. Этот код добавляет кнопку " Переименовать " в шаблон карты учетных данных; при нажатии эта кнопка вызовет функцию renameEl , которую мы только что создали:

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

Попробуйте! 👩🏻‍💻

  • Щелкните Переименовать .
  • Введите новое имя при появлении запроса.
  • Нажмите ОК .
  • Учетные данные должны быть успешно переименованы, и список должен обновиться автоматически.
  • При перезагрузке страницы должно отображаться новое имя (это показывает, что новое имя сохраняется на стороне сервера).

Показать дату создания учетных данных

Дата создания отсутствует в учетных данных, созданных с помощью navigator.credential.create() .

Но поскольку эта информация может быть полезна пользователю для различения учетных данных, мы изменили для вас серверную библиотеку в начальном коде и добавили поле creationDate , равное Date.now() , при сохранении новых учетных данных.

В templates.js в div class="creation-date" добавьте следующее, чтобы отображать информацию о дате создания для пользователя:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9. Сделайте свой код ориентированным на будущее

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

Еще одним продвинутым подходом может быть использование более мощного типа аутентификатора: роумингового аутентификатора с проверкой пользователя (UVRA). UVRA может обеспечить два фактора аутентификации и защиту от фишинга в потоках одноэтапного входа.

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

  • Если у пользователя есть только простой (не проверяющий пользователя) перемещаемый аутентификатор, пусть он использует его для обеспечения начальной загрузки учетной записи, защищенной от фишинга, но ему также придется ввести имя пользователя и пароль. Это то, что уже делает наша кодовая лаборатория.
  • Если у другого пользователя есть более продвинутый перемещаемый аутентификатор для проверки пользователя, он сможет пропустить шаг пароля — и, возможно, даже шаг имени пользователя — во время начальной загрузки учетной записи.

Узнайте больше об этом в статье «Защищенная от фишинга учетная запись с необязательным входом без пароля» .

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

Вам нужны две вещи:

  • Установите residentKey: preferred в настройках вашего бэкенда. Это уже сделано за вас.
  • Настройте способ узнать, были ли созданы обнаруживаемые учетные данные (также называемые резидентным ключом).

Чтобы узнать, были ли созданы обнаруживаемые учетные данные:

  • Запросите значение credProps при создании учетных данных ( credProps: true ).
  • Запросите значение transports при создании учетных данных. Это поможет вам определить, поддерживает ли базовая платформа функциональность UVRA, например, действительно ли это мобильный телефон.
  • Сохраните значение credProps и transports в бэкенде. Это уже было сделано за вас в стартовом коде. Взгляните на auth.js , если вам интересно.

Давайте получим значения credProps и transports и отправим их на серверную часть. В auth.client.js измените registerCredential следующим образом:

  • Добавьте поле extensions при вызове navigator.credentials.create
  • Установите encodedCredential.transports и encodedCredential.credProps перед отправкой учетных данных на серверную часть для хранения.

registerCredential должен выглядеть следующим образом:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10. Обеспечить кроссбраузерную поддержку

Поддержка браузеров, отличных от Chromium.

В функции registerCredential public/auth.client.js мы вызываем credential.response.getTransports() для вновь созданных учетных данных, чтобы в конечном итоге сохранить эту информацию в бэкэнде в качестве подсказки серверу.

Однако в настоящее время getTransports() реализован не во всех браузерах (в отличие от getClientExtensionResults , который поддерживается во всех браузерах): getTransports() вызовет ошибку в Firefox и Safari, что предотвратит создание учетных данных в этих браузерах.

Чтобы убедиться, что ваш код будет работать во всех основных браузерах, оберните вызов encodedCredential.transports условием:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

Обратите внимание, что на сервере для transports задано значение transports || [] . В Firefox и Safari список transports будет не undefined , а пустым списком [] , что предотвращает ошибки.

Предупреждать пользователей, использующих браузеры, не поддерживающие WebAuthn

1e9c1be837d66ce8.png

Несмотря на то, что WebAuthn поддерживается во всех основных браузерах, рекомендуется отображать предупреждение в браузерах, которые не поддерживают WebAuthn.

В index.html внимание на наличие этого div:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

Во встроенный скрипт index.html добавьте следующий код для отображения баннера в браузерах, не поддерживающих WebAuthn:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

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

11. Молодец!

✨Готово!

Вы внедрили двухфакторную аутентификацию с ключом безопасности.

В этой кодовой лаборатории мы рассмотрели основы. Если вы хотите подробнее изучить WebAuthn для 2FA, вот несколько идей о том, что вы можете попробовать дальше:

  • Добавьте информацию «Последнее использование» в карточку учетных данных. Это полезная информация для пользователей, чтобы определить, активно ли используется данный ключ безопасности, особенно если они зарегистрировали несколько ключей.
  • Реализуйте более надежную обработку ошибок и более точные сообщения об ошибках.
  • Загляните в auth.js и узнайте, что происходит, когда вы меняете некоторые authSettings , в частности, при использовании ключа, поддерживающего проверку пользователя.