Привет! В этой статье мы научимся добавлять WebAuthn в веб‑приложения со стороны frontend‑разработчика. WebAuthn представляет собой новый метод аутентификации, который обеспечивает более высокий уровень безопасности, заменяя устаревшие пароли и SMS‑подтверждения на аутентификацию на основе публичных ключей. Это не только повышает защиту от несанкционированного доступа, но и упрощает вход для пользователей. Например VK и другие компании уже переходят на подобные технологии, отходя от обычных паролей.

Основы публичного шифрования

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

Схема работы WebAuthn

WebAuthn
WebAuthn

WebAuthn работает на основе нескольких ключевых этапов:

  1. Создание вызова (challenge): Сервер генерирует уникальный вызов и отправляет его устройству пользователя.

  2. Подтверждение пользователя: Пользователь подтверждает свое намерение аутентифицироваться, используя биометрию, PIN‑код или другой метод.

  3. Подпись вызова: Устройство подписывает вызов своим приватным ключом, подтверждая легитимность запроса.

  4. Отправка утверждения (assertion): Устройство отправляет подписанный вызов обратно на сайт.

  5. Проверка утверждения: Сайт проверяет подпись с помощью ранее сохраненного публичного ключа. Успешная проверка подтверждает аутентификацию пользователя.

Пример реализации в веб-приложении

В этой части статьи мы займемся разработкой простого веб‑приложения, используя React.js. Наше приложение будет включать в себя одно поле для ввода имени пользователя и две функциональные кнопки: 'Регистрация' и 'Вход'.

Регистрация

Для регистрации нового пользователя, нам нужно реализовать функцию, запрашивающую создание новых учетных данных через navigator.credentials.create(). Важный компонент здесь — publicKeyCredentialCreationOptions, который определяет параметры для создания нового ключа. Для более подробного понимания этих параметров, рекомендую ознакомиться с документацией WebAuthn.

// Функция для создания новых учетных данных (credentials) с использованием Web Authentication API.
const getCredential = async ({
    challenge, // Хеш (вызов) который получаем от сервера
  }) => {
    // Опции для создания публичного ключа.
    const publicKeyCredentialCreationOptions = {
      rp: {
        name: 'my super app', // имя надежной стороны (Relying Party)
        id: 'webauthn.fancy-app.site', // идентификатор надежной стороны (Домен)
      },
      user: {
        id: Uint8Array.from(userId, (c) => c.charCodeAt(0)), // преобразование userId в Uint8Array
        name: 'User', // имя пользователя 
        displayName: 'Full username', // отображаемое имя пользователя
      },
      challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)), // преобразование challenge в Uint8Array
      pubKeyCredParams: [
        // предпочтительные параметры для криптографического алгоритма
        {
          type: 'public-key', // тип ключа
          alg: -7, // алгоритм ECDSA с кривой P-256
        },
        {
          type: 'public-key',
          alg: -257, // алгоритм RSA с ограничением SHA-256
        },
      ],
      timeout: 60000, // таймаут ожидания ответа (в миллисекундах)
      excludeCredentials: [], // список учетных данных, которые следует исключить
      authenticatorSelection: { // критерии выбора аутентификатора
        residentKey: 'preferred',
        requireResidentKey: false,
        userVerification: 'required', // требование верификации пользователя
      },
      attestation: 'none', // тип аттестации, здесь не требуется
      extensions: { // расширения
        credProps: true, // если true, возвращаются свойства учетных данных
      },
    };
    // API вызов для создания новых учетных данных с помощью переданных опций.
    return await navigator.credentials.create({
      publicKey: publicKeyCredentialCreationOptions,
    });
  };

Эта функция возвращает объект PublicKeyCredential, который в JavaScript представляет собой публичные учетные данные. Они создаются при регистрации через WebAuthn API.

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

PublicKeyCredential
PublicKeyCredential

Аутентификации

Для аутентификации пользователя нам потребуется создать функцию, применяющую navigator.credentials.get(). Эта функция будет использовать параметры publicKeyCredentialRequestOptions, которые должны включать challenge (вызов) и rpId (идентификатор надежного сайта, обычно домен)

const getAssertion = async ({ challenge }) => {
    // Создание объекта с параметрами запроса учетных данных (public key credential request options)
    const publicKeyCredentialRequestOptions = {
      // Преобразование challenge (предоставленного сервером) в Uint8Array. 
      // Это необходимо, так как WebAuthn требует, чтобы challenge был в бинарном формате.
      challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)),

      // Пустой массив, указывающий, что любые зарегистрированные учетные данные могут быть использованы.
      // Можно указать конкретные учетные данные, если требуется ограничить доступ.
      allowCredentials: [],

      // Идентификатор надежной стороны (Relying Party ID), обычно домен, с которым ассоциирован запрос.
      rpId: 'webauthn.fancy-app.site',

      // Время ожидания в миллисекундах для завершения процесса аутентификации.
      timeout: 60000,

      // Уровень проверки пользователя. В данном случае требуется подтверждение ('required').
      // Другие варианты могут включать 'preferred' или 'discouraged'.
      userVerification: 'required',
    };

    // Вызов метода get() объекта navigator.credentials с переданными параметрами.
    // Этот вызов инициирует процесс получения ассерции аутентификации от пользователя.
    return await navigator.credentials.get({
      publicKey: publicKeyCredentialRequestOptions,
    });
};

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

PublicKeyCredential
PublicKeyCredential

Демо

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

Спасибо за внимание)

Комментарии (1)


  1. AlexW-i
    15.11.2023 12:15

    Ваш Demo что-то не работает, по крайней мере на S23Ultra + Brave, браузер на компьютере запрашивает на какой девайс направить запрос, запрос на смартфон из браузера проходит, потом начитается анимация соединения, и все обрывается через полминуты с ошибкой. На странице демо тоже появляется сообщение об ошибке.