1. Усовершенствуем процесс аутентификации. Прошлая компания, в которой я работал: Werkspot. Она помогает людям, которым нужно сделать ремонт найти специалистов.
  2. Проблема

    Когда я исследовал наш основной сценарий, то обнаружил, что четверть пользователей теряется на шаге авторизации и увидел в этом возможность улучшить конверсию
  3. Конкретно этот график построен на базе отзывов пользователей, которые мы собираем когда пользователь пытается покинуть сценарий
  4. Почему?

    • Недостаток доверия
    • Недостаток информации
    • Слишком сложно
    Почему пользователи не хотят делиться своими приватными данными? Причин может быть множество, среди них: не достаток доверия, информации или это может быть слишком сложно сделать.
  5. Доверие

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

    Пользователь должен быть уверен, что вашим сервисом пользоваться безопасно.
  6. LastPass
    В своём отчете LastPass указала, что каждый пароль в среднем переиспользуется тринадцать раз. Вводя свой пароль пользователь выказывает вам значительное доверие
  7. Доверие

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

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

    Объясните, зачем вам нужна та или иная информация и как вы планируете её использовать.

    Прозрачность: Объясните, зачем вам нужна та или иная информация и как вы планируете её использовать.
  10. Мы добавили текст, который уточняет, что мы не передаем специалисту контактные данные пользователя, пока пользователь сам с ним не свяжеться
  11. Простота

    Не запрашивайте данные до того момента, пока они вам действительно не понадобятся

    Например, в одном из наших эксперементов, мы решили не запрашивать номер телефона пользователя до тех пор, пока его не нужно отправлять исполнителю.
  12. HTML

    Так как я верю в прогрессивное улучшение, давайте сначала обсудим что мы можем сделать с помощью HTML а уже потом — как это можно улучшить
  13. Формы

    И начнем мы с форм.
  14. Какие формы?

    • Регистрации
    • Логин
    • Восстановление пароля
    Каких форм? Если мы говорим об аутентификации, то осноыными связанными с ней формами будут форма Регистрации, Логина и Восстановления пароля
  15. Семантика!
    БИ!

    И что же нам поможет сделать формы лучше? Семантика!
  16. Атрибуты

    Что ещё следует иметь ввиду? Атрибуты!
  17. autocorrect="off"
    spellcheck="false"
    autocapitalize="none"
    Например вот эти вот позволят отключить атокорекцию, автокапитализацию и проверку правописания. Вряд ли вы хотите что бы они применялись к адресу электро-почты?
  18. Какие ещё атрибуты могут быть полезны?

    • inputmode
    • autocomplete
    Кроме этого стоит обратить внимание на inputmode и autocomplete.
  19. inputmode

    • none
    • text
    • tel
    • url
    • email
    • numeric
    • decimal
    • search
    inputmode определяет то, как выглядит виртуальная клавиатура для поля ввода
  20. autocomplete

    • username
    • new-password
    • current-password
    • one-time-code
    autocomplete подсказывает браузеру какие именно варианты выводить для автозаполнения
  21. Электронная почта

    inputmode="email"
    autocomplete="email"
    К примеру для поля электронной почты оба атрибута должны иметь значение email
  22. На iPhone вы получите варианты заполнения над клавиатурой и клавиатуру, которая оптимизирована для введения адреса электронной почты. Android сначала предложит заполнить всю форму разом, а только потом — выбрать конкретный адрес.
  23. Пароль при регистрации

    или восстановлении

    autocomplete="new-password"
    Ещё один полезный для формы регистрации атрибут это autocomplete new-password. Его имеет смысл использовать в форме регистрации, когда пароль вводится в первый раз.
  24. На iOS Safari для new-password покажет опцию Suggest New Password, которая сгенерирует сильный пароль, спросит можно ли ей его использовать и предложит его сохранить.
  25. Android поведет себя так же, за исключением того, что сохранит пароль без дополнительного разрешения.
  26. На десктопе всё работает аналогичным образом.
  27. Пароль при

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

    autocomplete="current-password"
    current-password это тип автозаполнения, который можно использовать в форме логина так как мы вводим уже существующий пароль
  28. iOS предложит заполнить пароль из связок логин-пароль, которые использовались на этом сайте. Android предложит заполнить всю форму, а затем выбрать пароль, который вы использовалии ранее. Оба покажут логин, с которым пароль использовался.
  29. Думаю все видели то, как это выглядит на десктопе
  30. Одноразовый пароль

    inputmode="numeric"
    autocomplete="one-time-code"
    one-time-code это одноразовый пароль, который обычно отправляют в виде SMS, по адресу электронной почты или генерируют с помощью приложения
  31. 
    `${code} — ваш код
      аутентификации.
      @${domain} #${code}`
                
    Давайте рассмотрим SMS, так как это именно тот случай, когда мы можем использовать автозаполнение
  32. Когда придет SMS, появится возможность автозаполнения
  33. И это особенно полезно, так как iOS может делиться SMS с MacOS.
  34. В результате на десктопе тоже появится подсказка
  35. Я дал вам формат для SMS сообщения, но все эти варианты, на деле, тоже сработают.
  36. iOS ищет номера рядом со словами:

    1. code
    2. passcode
    Дело в том, что до появления стандарта iOS просто искала номера рядом со словами code и passcode.
  37. Chrome и Android не поддерживают декларативный подход к автозаполнению одноразовых паролей

    Chrome и Android не поддерживают декларативный подход к автозаполнению одноразовых паролей
  38. Origin-bound one-time codes delivered via SMS

    Вот стандарт SMS с одноразовым паролем, который упоминался ранее. Это черновик, но он, с нюансами, поддерживается и iOS и Android.
  39. Automatic Strong Passwords and Security Code AutoFill

    Презентация Apple WWDC 2018, где представили возможности связанные с одноразовыми паролями и типом автозаполнения new-password
  40. HTML Living Standard

    Остальное вы можете найти в стандарте HTML
  41. Form Design Patterns Book
    Кроме того, обратите внимание на книгу «Шаблоны проэктироваиня форм». Думаю с ней вы сможете сделать ваши фромы ещё лучше.
  42. Откуда дровишки?!

    Хорошо, а откуда браузер берёт все эти данные для автозаполнения?
  43. Все ваши пароли
    принадлежат нам! БУП!

    Вы их сами ему даёте
  44. Браузер

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

    Все современные браузеры позволяют сохранять введенные вами реквизиты.
  45. А если это форма регистрации, то в выпадающем меню можно увидеть разные варианты полей, которые подходят в качестве логина.
  46. Н-а-з-о-в-и-и-и   м-о-о-о-ё   и-и-и-м-я!

    Не забывайте задавать имена полям. Это критически важно.
  47. <StyledInput
      ref={emailInputRef}
      type="email"
      onChange={setEmail}
      value={email}
      autoComplete="email"
      />
    Да, мы разрабатываем react приложения и редко отправляем формы, так, как это было задумано. Мы не предусматриваем деградации и всё это не будет работать без JavaScript. Есть соблазно не задавать имя.
  48. Но если мы не будем задавать имя, то есть шанс увидеть вот это. Телефонный номер, вместо логина. Как же так?!
  49. Chrome и Firefox

    1. username
    2. поле, которое находится перед паролем
    Chrome и FF ищут поле с именем username или autocomplete username. Если таких нет — они используют поле, которое в dom идет перед паролем.
  50. Safari

    1. username
    2. email
    3. поле, которое находится перед паролем
    Safari ведет себя немного более осторожно, и после username ищет email, а только затем — берет поле перед паролем.
  51. Подскажите браузеру

    где имя пользователя

    autocomplete="username email"
    Имея всё это ввиду, я бы рекомендовал использовать двойное значение для поля autocomplete. И задавать имена.
  52. Все пароли, платежные реквизиты и адреса, которые браузер сохранил, вы можете найти в его настройках
  53. К примеру вы можете управлять адресами, которые сохранил браузер. Удалять и редактировать.
  54. И, более того, добавлять новые. И это имеет смысл, так как в будущем сбережет вам массу времени при заполнении форм.
  55. Синхронизация данных

    • Google Password Manager
    • Firefox Lockwise
    • iCloud Keychain
    Особенно имея ввиду, что эти данные синхронизируются между различными устройствами.
  56. Google Password Manager
  57. Обычно Google стараеться тихонько включить ещё и синхронизацию в браузере с той же учетной записью. В любом из меню выберите «Manage your Google Account»
  58. Firefox Lockwise
  59. В меню браузера кликните «Logins and Passwords»
  60. И получите список сохраненных реквизитов
  61. iCloud Keychain
  62. Откройте настройки Safari, вкладку Passwords.
  63. iOS. Хорошо, и как телефон синхронизируется с десктопом?
  64. В iOS можно выбрать источники автозаполнения. По умолчанию это iCloud Keychain. Но можно выбрать ещё один. Это может быть как Google Password Manager Так и Lockwise FireFox.
  65. Изменение паролей

  66. Кроме автозаполнения браузер может находить скомпрометированные пароли. И, более того, вы можете их изменить прямо оттуда. Ну, почти.
  67. server.use(
    "/.well-known/change-password",
    (req, res) => {
      res.redirect("/reset-password");
    });
    Кнопка по умолчанию просто откроет домен, с которым связан пароль. Мы можем использовать этот url, что бы произошел редирект на страницу которая позволит изменить пароль. Это может быть просто страница восстановления пароля. Код, который вы видите будет работать на экспрессо-образных серверах.
  68. 
    server {
      rewrite
        ^/.well-known/change-password$
        http://domain.com/reset-password
        redirect;
    }
                  
    А вот этот — в NGINX.
  69. Expected responces

    • 302 Found
    • 303 See Other
    • 307 Temporary Redirect
    URL должен вернуть один из этих трех статусов.
  70. В Safari это работает с 2019 года
  71. А вот у FireFox это пока только в планах
  72. A Well-Known URL for Changing Passwords

    А вот это — стандарт, который описывает то, как это всё работает.
  73. JavaScript

    Ну, думаю вам уже порядком надоел HTML, так что давайте посмотрим, как именно мы можем использовать JavaScript
  74. Credential Management

    Практически всё, о чем мы будем говорить описано в спецификации Credential Management. Это особая спецификация. Расширяемая.
  75. navigator.credentials
    navigator.credentials это основа всего. Этот объект предоставляет нам четыре базовых метода для работы с реквизитами.
  76. .create(CredentialCreationOptions)
    .store(Credential)
    .get(CredentialRequestOptions)
    .preventSilentAccess()
    .create позволит создать объект с реквизитами, .store сохранить его в браузере, .get получить обратно. .preventSilentAccess отключает автологин и про это мы поговорим позднее.
  77. Credential types:

    • PasswordCredential
    • FederatedCredential
    • OTPCredential (WebOTP)
    • PublicKeyCredential (WebAuthn)
    На сегодняшний день есть четыре типа реквизитов.
  78. PasswordCredential

    Аутентификация с помощью имени пользователя и пароля.

    Самый базовый: пароль и логин.
  79. CredentialData:

    1. id
    Когда мы создаём объект с реквизитами, то тип CredentialData будет основой для любого из них. Он содержит один единственный атрибут — "id".
  80. PasswordCredential:

    1. name
    2. iconURL
    3. password
    4. type: "password"
    PasswordCredential расширяет CredentialData и добавляет в него поля name, iconURL, password и type. Который всегда будет иметь значение «password».
  81. У нас есть форма регистрации. Как только пользователь нажал кнопку «Зарегистрироваться»…
  82. const signUpHandler = async (event) => {
      // Регистрация
      const form = event.currentTarget;
      if (window?.PasswordCredential) {
        const credential = await navigator.
          credentials.create({
            password: form
          });
        navigator.credentials.store(credential);
      }
    }
    Мы создаём объект реквизитов прямо из объекта формы И сохраняем в браузере.
  83. const signUpHandler = (event) => {
      // Registration
      const form = event.currentTarget;
      if (window?.PasswordCredential) {
        const credential = new PasswordCredential(form);
        navigator.credentials.store(credential);
      }
    }
    Можно использовать конструктор PasswordCredential для читабельности
  84. Любопытный факт: если вы используете пароль сгенерированный браузером, он просто сохранит реквизиты, без дополнительного подтверждения со стороны пользователя
  85. В противном случае, при вызове метода store пользователь увидет всплывающее окно
  86. function signUpHandler (event) {
      // Регистрация
      const form = event.currentTarget;
      const data = new FormData(form);
    
      if (window?.PasswordCredential) {
        // …
      }
    }
    Но что если нам нужно больше контроля над содержимым объекта реквизитов?
  87. const credential =
      new PasswordCredential({
        id: data.get('email'),
        name: data.get('firstName'),
        password: data.get('password'),
        iconURL: data.get('avatar'),
      });
    navigator.credentials.store(credential);
    Мы можем задать данные вручную. Это позволит нам, к примеру, задать аватар и то, значение какого поля будет использоваться в качестве имени пользователя.
  88. В результате UI будет выглядеть намного более приятно
  89. navigator.credentials.get()
    Теперь, когда мы сохранили реквизиты, как же мы будем их использовать? Нужно запросить их у браузера используя метод get.
  90. CredentialRequestOptions:

    1. mediation
      • silent
      • optional
      • required
    2. signal
    Мы должны передать ему в качестве аргумента объект CredentialRequestOptions.
    Для понимания того как этот процесс работает критично понятие медиации.
  91. CredentialRequestOptions:

    1. mediation
      • silent
      • optional
      • required
    2. signal
    silent значит, что бразуер должен вернуть реквизиты без необходимости каких либо действий со стороны пользователя, если возможно, и null в противном случае.
    Браузер может вернуть реквизиты без подтверждения пользователя, если:
    1. У браузера есть реквизиты для этого домена
    2. У браузера только один набор реквизитов для этого домена
    3. Не был вызван метод Prevent Silent Access
  92. CredentialRequestOptions:

    1. mediation
      • silent
      • optional
      • required
    2. signal
    1. У браузера есть реквизиты для этого домена
    2. У браузера только один набор реквизитов для этого домена
    3. Не был вызван метод Prevent Silent Access
  93. CredentialRequestOptions:

    1. mediation
      • silent
      • optional
      • required
    2. signal
    optional значит, что бразуер должен вернуть реквизиты без необходимости каких либо действий со стороны пользователя, если возможно, и показать пользователю всплывающее окно, с запросом разрешения или выбором набора реквизитов, если нет. Вернет null, если реквизитов для домена нет или пользователь закрыл окно.
  94. CredentialRequestOptions:

    1. mediation
      • silent
      • optional
      • required
    2. signal
    required значит, что бразуер должен всегда запрашивать разрешения пользователя.
  95. CredentialRequestOptions:

    1. mediation
      • silent
      • optional
      • required
    2. signal
    signal это экземпляр AbortSignal. Он позволяет вам программно прервать запрос.
  96. PasswordCredential:

    1. password: true
    PasswordCredential расширяет CredentialRequestOptions и добавляет всего одно поле: password, значение котороего — true. Это значит, что браузер вернет только реквизиты типа password.
  97. (async () => {
      // Запускаем только для анонимов
      if (window?.PasswordCredential) {
        const credential =
          await navigator.credentials.get({
            password: true,
            mediation: 'silent',
          });
        if (credential === null) return;
        // Аутентификация
        // Обработка ошибок
    }})();
    Что бы сделать тихий логин, как в Twitter, нужно на каждой странице для анонимов разместить вот такой виджет. Он запрашивает реквизиты с mediation silent. Если вернуло — отлично, авторизуем. Нет — ну и ладно.
  98. PasswordCredentialData:

    1. id
    2. name
    3. iconURL
    4. password
    5. type: "password"
    Внутри объекта реквизитов ровно то, что мы туда положили
  99. В случае успешной аутентификации появится всплывающее окно
  100. if (window?.PasswordCredential) {
      const credential =
        await navigator.credentials.get({
          password: true,
          mediation: 'optional',
        });
      if (credential === null) return;
      // Аутентификация
      // Обработка ошибок
    }
    Когда пользователь нажал в меню кнопку логин мы можем запросить реквизиты с mediation: 'optional'. Пользователь сам выразил желание войти в учетную запись и всплывающее окно будет реакцией на его действия.
  101. Если он выберет учетную запись или даст разрешение на аутентификацию — отлично. Если нет — можно открыть обычную страницу логина.
  102. if (navigator?.credentials
        .preventSilentAccess) {
      await navigator?.credentials
        .preventSilentAccess(id);
    }
    После того как пользователь нажал логаут, нужно вызвать метод preventSilentAccess. Иначе при обновлении страницы его залогинит обартно и это вряд ли будет желаемое поведение.
  103. Но самым важным для нас является кросс-платформенный логин. С его помощью мы можем залогиниться на десктопе, и автоматически потом логиниться на мобильном устройстве.
  104. Давайте посмотрим как это работает! У меня нет телефона на Android, так что я использую эмулятор. Я запустил его предварительно и вошел в свою учетную запись google.
  105. FederatedCredentialData:

    • id
    • name
    • iconURL
    • provider
    • protocol
    • type: "federated"
    Вторым типом реквизитов является FederatedCredentialData. То же что и авторизация с помощью логина и пароля, но используя социальный логин. Я не буду останавливаться на нем подробно из за ограниченых временных рамок.
  106. Поддерживается API в Blink. То-есть, в большинстве браузеров. Так же указана поддержка iOS, но это не совсем правда.
  107. Apple скомно заявляет о поддержке, но на самом деле, это касается исключительно PublicKeyCredential.
  108. Password и FederatedCredential похоже ждать не приходится.
  109. Mozilla не считает это API приоритетным для себя, так что особой надежды увидеть его поддержку в скором времени нет.
  110. The Credential Management API (Web Fundamentals)

    Почитайте отличную статью о Credential Management от Google.
  111. Credential Management

    И, конечно, спецификацию. Последние правки в которую были внесены не далее, как 16 ноября.
  112. Web OTP API

    Помните, как мы в первой части делали автозаполнение OTP в Safari? Я говорил, что Android не поддерживает декларативное заполнение.
  113. OTPCredential

    Вместо этого он поддерживает тип реквизитов OTPCredential и позволяет делать то же самое, только ещё лучше.
  114. if ('OTPCredential' in window) {
      const { code, type } = await
        navigator.credentials.get({
          otp: { transport: ['sms'] }
        });
    
      if (!code) return;
      // Аутентификация
    }
    Запрашиваем реквизиты с типом otp и транспортом sms.
  115. Браузер запросит разрешение у пользователя прочесть SMS. И затем вернет код.
  116. Требования к SMS:

    1. Сообщение должно содержать 4-10 знакочисловых символов, Как минимум один из которых — число
    В сообщении должно быть 4-10 символьные знакочисловой код. Как минимум один из этих символов должен быть числом.
  117. Требования к SMS:

    1. Сообщение должно быть отправлено с номера, которого нет в контактной книге пользователя.
    Таким образом спецификация гарантирует, что из JavaScript не удастся получить доступ к приватной переписке.
  118. Verify phone numbers on the web with the Web OTP API (Web Fundamentals)

    Отличная тематическая статья на Web Fundamentals
  119. Origin-bound one-time codes delivered via SMS

    Спецификация формата SMS
  120. Web OTP API

    Спецификация касающаяся одноразовых паролей
  121. WebAuthn

  122. PublicKeyCredential

  123. Кросс-платформенные

  124. Платформенные

  125. Face unlock
  126. Платформенный ключ:

    1. Пользователь регистрируется
    2. Запрашиваем разрешения использовать ключ
    3. Если пользователь согласен: регистрируем ключ и храним id в браузере
    4. При попытке аутентификации — используем ключ
    Если мы говорим об упрощении аутентификации, то нас интересует именно платформенный ключ
  127. Регистраця ключа:

    1. Передаем серверу данные о платформе и пользователе, получаем в ответ проверочный объект
    2. Передаём его платформе и получаем созданные на его базе PublicKeyCredential
    3. Отправляем PublicKeyCredential на сервер для проверки
    4. Получаем Сredential Id, который можем сохранить на клиете
  128. if ('PublicKeyCredential' in window) {
      /* … */
    }
  129. const hasPlatformAuthenticator
      = await PublicKeyCredential
      ?.isUserVerifyingPlatformAuthenticatorAvailable();
  130. PublicKeyCredential
      .is
        User
        Verifying
        Platform
        Authenticator
        Available();
  131. const data = new FormData(form);
    const response = await fetch(
      '/challenge/register',
      {
        method: 'POST',
        body: JSON.stringify({
          name: data.get('name'),
          email: data.get('email'),
          'new-password': data.get('new-password'),
        }),
      });
    
    const options = await response.json();
  132. const credential = await navigator.credentials
      .create({
        publicKey: options
      });
  133. {
      id: "nZOlyXf57erlQbudIIuOrQEdRUs",
      rawId: [ArrayBuffer],
      response: {
        attestationObject: [ArrayBuffer],
        clientDataJSON: [ArrayBuffer],
      },
      type: "public-key",
    }
  134. const body = JSON.stringify(attestation);
    
    const response = await fetch(`/register`, {
      method: "POST",
      body,
    });
  135. const { credentialId } = await response.json();
    localStorage.setItem('credentialId', credentialId);
  136. Логин с помощью платформенного ключа:

    1. Проверяем есть ли сохраненный Сredential Id
    2. Запрашиваем на сервере проверочный объект
  137. Логин с помощью платформенного ключа:

    1. Передаём его платформе и получаем PublicKeyCredential
    2. Отправляем PublicKeyCredential на сервер для проверки
  138. const credentialId = localStorage.getItem('credentialId');
    if (credentialId === null) return;
  139. const response = await fetch(
      '/challenge/login',
      { method: 'POST' }
    );
    
    const options = await response.json();
  140. const { challenge } = options;
    const credentialId = localStorage.getItem('credentialId');
    options.allowCredentials = [{
      type: "public-key",
      id: toBuffer(credentialId),
    }];
  141. const credential = await navigator.credentials.get({
      publicKey: options
    });
  142. {
      id: "FtjWT4fIT9…zs2LRxuG5fBRj9bA",
      rawId: [ArrayBuffer],
      response: {
        authenticatorData: [ArrayBuffer],
        clientDataJSON: [ArrayBuffer],
        signature: [ArrayBuffer],
        userHandle: null,
      },
      type: "public-key",
    }
  143. const body = JSON.stringify(authenticator);
    await fetch('/login', {
      method: "POST",
      body
    });
  144. WebAuthn demo application

  145. Emulate Authenticators and Debug WebAuthn in Chrome DevTools

  146. A curated list of awesome WebAuthn/FIDO2 resources

  147. WebAuthn guide

  148. Introduction to WebAuthn API (Yuriy Ackermann)

  149. Meet Face ID and Touch ID for the web (WWDC 2020 video)

  150. Meet Face ID and Touch ID for the web (Webkit blog post)

  151. Web Authentication and Windows Hello

  152. Enabling Strong Authentication with WebAuthn

  153. Fido Alliance

  154. Verifiable Credentials Data Model

  155. Yubico (YubiKey)

  156. Web Authentication: An API for accessing Public Key Credentials

  157. QRCODE with presentation link