1. Tech

    Как упростить аутентификацию для пользователя

    Антон Немцев

  2. Проблема

  3. Почему нет?

    • Нет доверия
    • Недостаточно информации
    • Сложно
  4. Доверие

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

  5. LastPass
  6. Доверие

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

  7. Прозрачность

    Расскажите зачем вам нужна та или иная информация и как она будет использоваться. А также — как не будет.

  8. Простота

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

  9. HTML

  10. Формы

  11. Какие формы?

    • Регистрация
    • Логин
    • Восстановление пароля
  12. Семантика!
    БИП!

  13. <form method="post" action="/sign-up/">
      <fieldset>
        <legend>Регистрация</legend>
        <!-- Поля ввода -->
      </fieldset>
      <button type="submit">
        Зарегистрироваться
      </button>
    </form>
  14. <label>
        Email
        <small>Мы не будем спамить</small>
        <input 
          value=""
          name="email"
          type="email" 
          placeholder="email@gmail.com">
      </label>
  15. Атрибуты

  16. autocorrect="off"
    spellcheck="false"
    autocapitalize="none"
  17. Какие ещё атрибуты будут нам полезны?

    • inputmode
    • autocomplete
  18. inputmode

    • none
    • text
    • tel
    • url
    • email
    • numeric
    • decimal
    • search
  19. autocomplete

    • email
    • tel
    • name
    • given-name
    • family-name
  20. autocomplete

    • username
    • new-password
    • current-password
    • one-time-code
  21. inputmode="email"
    autocomplete="email"
  22. autocomplete="new-password"
  23. autocomplete="current-password"
  24. inputmode="numeric"
    autocomplete="one-time-code"
  25. 
    `${code} is your 
     authentication code.
     @${domain} #${code}`
            
  26. iOS ищет слова в близости к цифрам

    1. code
    2. passcode
  27. Chrome на Android не поддерживает декларативный подход к автозаполнению OTP

  28. Automatic Strong Passwords and Security Code AutoFill

  29. Origin-bound one-time codes delivered via SMS

  30. HTML Living Standard

  31. Книга Form Design Patterns
  32. Источник данных

  33. Твои пароли
    зохвачены! БИП!

  34. Зада-а-a-a-ай na-a-a-a-ame!

  35. <StyledInput
      ref={emailInputRef}
      type="email"
      onChange={setEmail}
      value={email}
      autoComplete="email"
      />
  36. Chrome и Firefox

    1. username
    2. поле перед паролем
  37. Safari

    1. username
    2. email
    3. поле перед паролем
  38. autocomplete="username email"
  39. Восстановление пароля

  40. server.use(
      "/.well-known/change-password", 
      (req, res) => {
        res.redirect("/reset-password");
    });
  41. 
    server {
      rewrite 
        ^/.well-known/change-password$ 
        http://domain.com/reset-password
        redirect;
    }
    
  42. Ожидаемые ответы

    • 302 Found
    • 303 See Other
    • 307 Temporary Redirect
  43. server.use(
      "/.well-known/resource-that-
      should-not-exist-whose-status-
      code-should-not-be-200",
      (req, res) => {
        res.status(404).end();
      });
  44. Ожидаемый ответ

    • 404 Not Found
  45. A Well-Known URL for Changing Passwords

  46. JavaScript

  47. Credential Management

  48. 
    navigator.credentials
            
  49. 
    .create(CredentialCreationOptions)
    .store(Credential)
    .get(CredentialRequestOptions)
    .preventSilentAccess()
            
  50. Типы Credential:

    1. PasswordCredential
    2. FederatedCredential
    3. OTPCredential (WebOTP)
    4. PublicKeyCredential (WebAuthn)
  51. PasswordCredential

    Регистрация с помощью логина и пароля.

  52. CredentialData:

    1. id
  53. PasswordCredential:

    1. name
    2. iconURL
    3. password
    4. type: "password"
  54. const signUpHandler = async (event) => { 
      // Регистрация
      const form = event.currentTarget;
      if (window?.PasswordCredential) {
        const credential = await navigator.
          credentials.create({
            password: form
          });
        navigator.credentials.store(credential);
      }
    }
  55. const signUpHandler = (event) => { 
      // Регистрация
      const form = event.currentTarget;
      if (window?.PasswordCredential) {
        const credential = 
          new PasswordCredential(form);
        navigator.credentials.store(credential);
      }
    }
  56. function signUpHandler (event) { 
      // Регистрация
      const form = event.currentTarget;
      const data = new FormData(form);
    
      if (window?.PasswordCredential) {
        // …
      }
    }
  57. 
    const credential = 
      new PasswordCredential({
        id: data.get('email'),
        name: data.get('firstName'),
        password: data.get('password'),
        iconURL: data.get('avatar'),
      });
    navigator.credentials.store(credential);
            
  58. navigator.credentials.get()
  59. CredentialRequestOptions:

    1. mediation
      • silent
      • optional
      • required
    2. signal
  60. PasswordCredential:

    1. password: true
  61. (async () => {
      if (window?.PasswordCredential) {
        const credential = 
          await navigator.credentials.get({
            password: true,
            mediation: 'silent',
          });
        if (credential === null) return;
        // Авторизация
        // Обработка ошибок
    }})();
  62. PasswordCredentialData:

    1. id
    2. name
    3. iconURL
    4. password
    5. type: "password"
  63. if (window?.PasswordCredential) {
      const credential = 
        await navigator.credentials.get({
          password: true,
          mediation: 'optional',
        });
      if (credential === null) return;
      // Авторизация
      // Обработка ошибок
    }
  64. 
    if (navigator?.credentials
          ?.preventSilentAccess) {
      await navigator?.credentials
          ?.preventSilentAccess(id);
    }
            
  65. FederatedCredentialData:

    • id
    • name
    • iconURL
    • provider
    • protocol
    • type: "federated"
  66. The Credential Management API (Web Fundamentals)

  67. Credential Management

  68. Web OTP API

  69. OTPCredential

  70. if ('OTPCredential' in window) {
      const { code, type } = await 
        navigator.credentials.get({
          otp: { transport:['sms'] }
        });
    
      if (!code) return;
      // Авторизация
    }
  71. Требования к SMS в Chrome под Android:

    1. Сообщение должно содержать 4-10 число-буквенных символов, минимум 1 из которых число
  72. Требования к SMS в Chrome под Android:

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

  74. Origin-bound one-time codes delivered via SMS

  75. Web OTP API

  76. isLoggedIn Web API

  77. 
    navigator.setLoggedIn
    navigator.setLoggedOut
    navigator.isLoggedIn
            
  78. isLoggedIn
    Web API

  79. WebAuthn

  80. PublicKeyCredential

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

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

  83. Платформенный ключ:

    1. Пользователь регистрируется
    2. Мы спрашиваем разрешения для использования платформенного ключа
    3. Если пользователь соглашается регистрируем ключ и храним id в браузере
    4. При попытке аутентификации — используем
  84. Кросс-платформенный ключ:

    1. Пользователь регистрируется
    2. Даем ему возможность по его инициативе добавить ключи в профиле и храним их id на сервере
    3. При попытке аутентификации пользователь вводит логин и сервер присылает связанный список ключей
    4. Если ключи есть вместо или после ввода пароля авторизируется ключем с пином.
  85. Регистрация ключа:

    1. Запрашиваем у сервера вызов (challenge), данные о платформе и пользователе
    2. Передаем их ключу и получаем PublicKeyCredential
  86. Регистрация ключа:

    1. Отправляем PublicKeyCredential серверу для валидации и сохранения
    2. Получаем Сredential Id который следует хранить на клиенте
  87. 
    if ('PublicKeyCredential' in window) {
      /* … */ 
    }
            
  88. const hasPlatformAuthenticator 
      = await PublicKeyCredential
      ?.isUserVerifyingPlatformAuthenticatorAvailable();
  89. PublicKeyCredential
      .is
       User
       Verifying
       Platform
       Authenticator
       Available();
  90. 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();
  91. npm i -S fido2-library
  92. const { Fido2Lib } 
      = require("fido2-library");
    
    const f2l = new Fido2Lib({
      rpId: HOST,
      attestation: "none",
      authenticatorAttachment: "platform",
      authenticatorUserVerification: 
        "discouraged",
    });
  93. const f2l = new Fido2Lib({
      rpId: HOST,
      attestation: "none",
      authenticatorAttachment: "cross-platform",
      authenticatorUserVerification: "required",
    });
  94. fastify.post('/challenge/register/',
      async (request, reply) => {
        const { 
          email, 
          name 
        } = JSON.parse(request.body);
        // Регистрация пользователя
        // …
    });
  95. const options 
      = await f2l.attestationOptions();
  96. {
      🔴 rp: {
        🔴 name 
        🟢 id // origin
        🟢 icon
      },
      🔴 user: {
        🔴 id,❗️
        🔴 displayName,
        🔴 name,
        🟢 icon,
      },
      🔴 challenge,❗
      🔴 pubKeyCredParams,
    
    
      🟢 timeout,
      🟢 excludeCredentials,
      🟢 authenticatorSelection: {
        authenticatorAttachment: [
          "platform",
          "cross-platform"
        ],
        requireResidentKey,
        userVerification: [
          "required",
          "preferred",
          "discouraged"
        ]
      },
      🟢 attestation,
      🟢 extensions,
    }
  97. {
      rp: { 
        name: 'Anonymous Service', 
        id: 'nice-cheetah-30.loca.lt' },
      user: {
        id: [ArrayBuffer],
        name: 'thesilentimp@gmail.com',
        displayName: 'Антон Немцев' },
      challenge: [ArrayBuffer],
      pubKeyCredParams: [
        { type: 'public-key', alg: -7 },
        { type: 'public-key', alg: -257 }
      ],
      timeout: 60000,
      attestation: 'none',
      authenticatorSelection: { 
        userVerification: 'discouraged' }}
  98. const toBuffer = (base64) => {
      const ASCIIString = atob(base64);
            
      return Uint8Array.from(
        ASCIIString, 
        c => c.charCodeAt(0)
      ).buffer;
    }
  99. const toBase64 = (buffer) => {
      const binaryString = 
        String.fromCharCode.apply(
          null, 
          new Uint8Array(buffer));
    
      return btoa(binaryString);
    }
  100. const challengeString = 
      toBuffer(options.challenge);
    
    request.session.set(
      'challenge', 
      challengeString);
  101. options.challenge = 
      challengeString;
    
    options.user = {
      id: nanoid(), // Хэш id из базы
      name: email,
      displayName: name,
    };
  102. 
    reply
      .type('application/json')
      .code(200)
      .send(options);
            
  103. options.user.id = 
      toBuffer(options.user.id);
    
    options.challenge = 
      toBuffer(options.challenge);
    
    const credential = 
      await navigator.credentials.create({
        publicKey: options
      });
  104. {
      id: "nZOlyXf57erlQbudIIuOrQEdRUs",
      rawId: [ArrayBuffer],
      response: {
        attestationObject: [ArrayBuffer],
        clientDataJSON: [ArrayBuffer],
      },
      type: "public-key",
    }
  105. const {
      id,
      type,
      response: {
        clientDataJSON: clientDataBuffer,
        attestationObject: attestationBuffer,
    }} = credential;
    
  106. const clientDataJSON = 
      toBase64(clientDataBuffer);
    
    const attestationObject = 
      toBase64(attestationBuffer);
  107. const attestation = {
      id,
      type,
      response: {
        clientDataJSON,
        attestationObject,
      },
    };
  108. const body = 
      JSON.stringify(attestation);
    
    const response = 
      await fetch(`/register`, {
        method: "POST",
        body });
  109. const { credentialId } = 
      await response.json();
    
    localStorage
      .setItem(
        'credentialId', 
        credentialId);
  110. fastify.post('register/',
      async (request, reply) => {
        const { session } = request;
        const challenge = 
          session.get('challenge');
    
        // …
    });
  111. const assertionExpectations = {
      challenge,
      origin: `https://${HOST}`,
      factor: "either"
    };
  112. 
    const clientAttestationResponse = 
      JSON.parse(request.body);
    
    clientAttestationResponse.rawId = 
      toBuffer(clientAttestationResponse.Id);
            
  113. 
    const result = 
      await f2l.attestationResult(
        clientAttestationResponse, 
        assertionExpectations);
    
    const { authnrData } = result;
            
  114. const counter = 
      authnrData.get('counter');
    const credentialId = 
      authnrData.get('credId');
    const publicKey = 
      authnrData.get('credentialPublicKeyPem');
    
    const { session } = request;
    session.set('counter', counter);
    session.set('credentialId', credentialId);
    session.set('publicKey', publicKey);
  115. const credentialIdBuffer 
      = authnrData.get('credId')
    const credentialId 
      = toBase64(credentialIdBuffer);
    
    reply
      .type('application/json')
      .code(200)
      .send({credentialId});
  116. Логин с помощью ключа:

    1. Проверяем есть ли у нас Сredential Id
    2. Запрашиваем у сервера вызов (challenge) и данные о платформе
  117. Логин с помощью ключа:

    1. Передаем их ключу и получаем PublicKeyCredential
    2. Отправляем PublicKeyCredential серверу для валидации
  118. 
    const credentialId = 
      localStorage.getItem('credentialId');
    
    if (credentialId === null) return;
            
  119. const response = await fetch(
      '/challenge/login', 
      { method: 'POST' });
    
    const options = await response.json();
  120. fastify.post(
      '/challenge/login',
      async (request, reply) => {
        
      // …
    });
  121. const options = 
      await f2l.assertionOptions();
  122. {
      challenge: [ArrayBuffer],
      timeout: 60000,
      rpId: HOST,
      userVerification: 'discouraged'
    }
  123. const { session } = request;
    const challenge = 
      toBuffer(option.challenge);
    session.set('challenge', challenge);
    option.challenge = challenge;
    reply
      .type('application/json')
      .code(200)
      .send(option);
  124. const { challenge } = options;
    options.challenge = toBuffer(challenge);
    const credentialId = 
      localStorage.getItem('credentialId');
    options.allowCredentials = [{ 
      type: "public-key", 
      id: toBuffer(credentialId),
    }];
  125. 
    const credential = 
      await navigator.credentials.get({
        publicKey: options
      });
            
  126. {
      id: "FtjWT4fIT9…zs2LRxuG5fBRj9bA",
      rawId: [ArrayBuffer],
      response: {
        authenticatorData: [ArrayBuffer],
        clientDataJSON: [ArrayBuffer],
        signature: [ArrayBuffer],
        userHandle: null,
      },
      type: "public-key",
    }
  127. const {
      id,
      type,
      response: {
        authenticatorData: authenticatorDataBuffer,
        clientDataJSON: clientDataJSONBuffer,
        signature: signatureBuffer,
        userHandle,
      }
    } = credential;
  128. 
    const authenticatorData = 
      toBase64(authenticatorDataBuffer);
    const clientDataJSON = 
      toBase64(clientDataJSONBuffer);
    const signature = 
      toBase64(signatureBuffer);
          
  129. const authenticator = {
      id,
      type,
      response: {
        authenticatorData,
        clientDataJSON,
        signature,
        userHandle,
      },
    };
  130. const body = 
      JSON.stringify(authenticator);
    await fetch('/login', { 
        method: "POST",
        body 
      });
  131. const { session } = request;
    const challenge = 
      session.get('challenge');
    const publicKey =
      session.get('publicKey');
    const prevCounter = 
      session.get('counter');
  132. const assertionExpectations = {
      challenge,
      origin: `https://${HOST}`,
      factor: "either",
      publicKey,
      prevCounter,
      userHandle,
    };
  133. const clientAssertionResponse = 
      JSON.parse(request.body);
    clientAssertionResponse.rawId = 
      toBuffer(clientAssertionResponse.id);
    
    await f2l.assertionResult(
      clientAssertionResponse, 
      assertionExpectations
    );
  134. 
    reply.code(204);
            
  135. Приложение, с помощью которого я показал работу WebAuthn

  136. A curated list of awesome WebAuthn/FIDO2 resources

  137. WebAuthn guide

  138. Introduction to WebAuthn API (Yuriy Ackermann)

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

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

  141. Web Authentication and Windows Hello

  142. Enabling Strong Authentication with WebAuthn

  143. Emulate Authenticators and Debug WebAuthn in Chrome DevTools

  144. Fido Alliance

  145. Yubico (YubiKey)

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

  147. UP2IT IT-CONFERENCE

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

  148. QRCODE cо ссылкой на доклад