اولین برنامه WebAuthn خود را بسازید

1. قبل از شروع

Web Authentication API که با نام WebAuthn نیز شناخته می‌شود، به شما امکان می‌دهد اعتبارنامه‌های کلید عمومی با محدوده مبدا را برای احراز هویت کاربران ایجاد و استفاده کنید.

API از استفاده از احراز هویت BLE، NFC و رومینگ USB-U2F یا FIDO2 - که به عنوان کلیدهای امنیتی نیز شناخته می شود - و همچنین یک تأیید کننده پلت فرم که به کاربران امکان می دهد با اثر انگشت یا قفل صفحه خود احراز هویت کنند، پشتیبانی می کند.

در این کد لبه، شما یک وب سایت با یک قابلیت احراز هویت مجدد ساده می سازید که از حسگر اثر انگشت استفاده می کند. احراز هویت مجدد از داده‌های حساب محافظت می‌کند زیرا کاربرانی را که قبلاً وارد یک وب‌سایت شده‌اند می‌خواهد وقتی سعی می‌کنند وارد بخش‌های مهم وب‌سایت شوند یا پس از مدت زمان مشخصی دوباره از وب‌سایت بازدید کنند، دوباره احراز هویت کنند.

پیش نیازها

  • درک اولیه از نحوه عملکرد WebAuthn
  • مهارت های اولیه برنامه نویسی با جاوا اسکریپت

کاری که خواهی کرد

  • وب سایتی با قابلیت احراز هویت مجدد ساده بسازید که از حسگر اثر انگشت استفاده می کند

آنچه شما نیاز دارید

  • یکی از دستگاه های زیر:
    • یک دستگاه اندروید، ترجیحا با سنسور بیومتریک
    • یک iPhone یا iPad با Touch ID یا Face ID در iOS 14 یا بالاتر
    • MacBook Pro یا Air با Touch ID در macOS Big Sur یا بالاتر
    • Windows 10 19H1 یا بالاتر با راه اندازی Windows Hello
  • یکی از مرورگرهای زیر:
    • Google Chrome 67 یا بالاتر
    • مایکروسافت اج 85 یا بالاتر
    • سافاری 14 یا بالاتر

2. راه اندازی شوید

در این کد لبه از سرویسی به نام glitch استفاده می کنید. اینجاست که می‌توانید کدهای سرویس گیرنده و سمت سرور را با جاوا اسکریپت ویرایش کنید و آن‌ها را فوراً اجرا کنید.

به https://glitch.com/edit/#!/webauthn-codelab-start بروید .

ببینید چگونه کار می کند

برای مشاهده وضعیت اولیه وب سایت مراحل زیر را دنبال کنید:

  1. کلیک 62bb7a6aac381af8.png نمایش > 3343769d04c09851.png در یک پنجره جدید برای دیدن وب سایت زنده .
  2. نام کاربری دلخواه خود را وارد کنید و روی Next کلیک کنید.
  3. یک رمز عبور وارد کنید و روی Sign-in کلیک کنید.

رمز عبور نادیده گرفته شده است، اما شما هنوز احراز هویت هستید. شما در صفحه اصلی فرود می آیید.

  1. روی Try reauth کلیک کنید و مراحل دوم، سوم و چهارم را تکرار کنید.
  2. روی خروج کلیک کنید.

توجه داشته باشید که هر بار که سعی می‌کنید وارد شوید، باید رمز عبور را وارد کنید. این رمز کاربری را شبیه‌سازی می‌کند که قبل از اینکه بتواند به بخش مهمی از وب‌سایت دسترسی پیدا کند، نیاز به احراز هویت مجدد دارد.

کد را دوباره میکس کنید

  1. به WebAuthn / FIDO2 API Codelab بروید .
  2. روی نام پروژه خود > Remix Project کلیک کنید 306122647ce93305.png پروژه را منشعب کنید و با نسخه خود در یک URL جدید ادامه دهید.

8d42bd24f0fd185c.png

3. ثبت اعتبار با اثر انگشت

شما باید یک اعتبار ایجاد شده توسط یک UVPA، یک احراز هویت که در دستگاه تعبیه شده است و هویت کاربر را تأیید می کند، ثبت کنید. این معمولا به عنوان حسگر اثر انگشت بسته به دستگاه کاربر دیده می شود.

شما این ویژگی را به صفحه /home اضافه می کنید:

260aab9f1a2587a7.png

تابع registerCredential() را ایجاد کنید

یک تابع registerCredential() ایجاد کنید که یک اعتبار جدید را ثبت می کند.

public/client.js

export const registerCredential = async () => {

};

چالش و گزینه های دیگر را از نقطه پایانی سرور دریافت کنید

قبل از اینکه از کاربر بخواهید یک اعتبارنامه جدید ثبت کند، از سرور درخواست کنید که پارامترها را برای ارسال در WebAuthn، از جمله یک چالش، بازگرداند. خوشبختانه، شما در حال حاضر یک نقطه پایانی سرور دارید که با چنین پارامترهایی پاسخ می دهد.

کد زیر را به registerCredential() اضافه کنید.

public/client.js

const opts = {
  attestation: 'none',
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    userVerification: 'required',
    requireResidentKey: false
  }
};

const options = await _fetch('/auth/registerRequest', opts);

پروتکل بین سرور و کلاینت بخشی از مشخصات WebAuthn نیست. با این حال، این کد لبه برای همسویی با مشخصات WebAuthn طراحی شده است و شی JSON که به سرور ارسال می کنید بسیار شبیه به PublicKeyCredentialCreationOptions است تا برای شما بصری باشد. جدول زیر شامل پارامترهای مهمی است که می توانید به سرور منتقل کنید و توضیح می دهد که آنها چه کاری انجام می دهند:

مولفه های

توضیحات

attestation

اولویت برای انتقال گواهی - none ، indirect یا direct . none را انتخاب نکنید مگر اینکه به آن نیاز داشته باشید.

excludeCredentials

آرایه ای از PublicKeyCredentialDescriptor به طوری که احراز هویت کننده بتواند از ایجاد موارد تکراری جلوگیری کند.

authenticatorSelection

authenticatorAttachment

احراز هویت موجود را فیلتر کنید. اگر می خواهید یک احراز هویت به دستگاه متصل شود، از " platform " استفاده کنید. برای احراز هویت رومینگ، از " cross-platform " استفاده کنید.

userVerification

تعیین کنید که آیا تأیید اعتبار کاربر محلی « required است»، « preferred است» یا « discouraged » است. اگر تأیید هویت با اثر انگشت یا قفل صفحه را می‌خواهید، از « required » استفاده کنید.

requireResidentKey

اگر اعتبار ایجاد شده باید برای UX انتخابگر حساب آینده در دسترس باشد، از true استفاده کنید.

برای کسب اطلاعات بیشتر در مورد این گزینه ها، به 5.4 مراجعه کنید. گزینه‌های Credential Creation (لغت نامه PublicKeyCredentialCreationOptions ) .

موارد زیر نمونه گزینه هایی هستند که از سرور دریافت می کنید.

{
  "rp": {
    "name": "WebAuthn Codelab",
    "id": "webauthn-codelab.glitch.me"
  },
  "user": {
    "displayName": "User Name",
    "id": "...",
    "name": "test"
  },
  "challenge": "...",
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    }, {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "userVerification": "required"
  }
}

اعتبارنامه ایجاد کنید

  1. از آنجایی که این گزینه ها به صورت کدگذاری شده برای عبور از پروتکل HTTP تحویل داده می شوند، برخی از پارامترها را به باینری بازگردانید، به طور خاص، user.id ، challenge و نمونه هایی از id موجود در آرایه excludeCredentials :

public/client.js

options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  1. برای ایجاد یک اعتبار جدید، متد navigator.credentials.create() را فراخوانی کنید.

با این تماس، مرورگر با احراز هویت کننده تعامل دارد و سعی می کند هویت کاربر را با UVPA تأیید کند.

public/client.js

const cred = await navigator.credentials.create({
  publicKey: options,
});

هنگامی که کاربر هویت خود را تأیید کرد، باید یک شی اعتبار دریافت کنید که می توانید آن را به سرور ارسال کنید و تأیید کننده را ثبت کنید.

اعتبارنامه را در نقطه پایانی سرور ثبت کنید

در اینجا نمونه ای از شی اعتبارنامه است که باید دریافت می کردید.

{
  "id": "...",
  "rawId": "...",
  "type": "public-key",
  "response": {
    "clientDataJSON": "...",
    "attestationObject": "..."
  }
}
  1. مانند زمانی که یک شی گزینه برای ثبت اعتبار دریافت کردید، پارامترهای باینری اعتبارنامه را رمزگذاری کنید تا بتوان آن را به عنوان یک رشته به سرور تحویل داد:

public/client.js

const credential = {};
credential.id = cred.id;
credential.rawId = base64url.encode(cred.rawId);
credential.type = cred.type;

if (cred.response) {
  const clientDataJSON =
    base64url.encode(cred.response.clientDataJSON);
  const attestationObject =
    base64url.encode(cred.response.attestationObject);
  credential.response = {
    clientDataJSON,
    attestationObject,
  };
}
  1. شناسه اعتبار را به صورت محلی ذخیره کنید تا بتوانید هنگام بازگشت کاربر از آن برای احراز هویت استفاده کنید:

public/client.js

localStorage.setItem(`credId`, credential.id);
  1. شی را به سرور ارسال کنید و اگر HTTP code 200 ، اعتبار جدید را با موفقیت ثبت کنید.

public/client.js

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

شما اکنون تابع registerCredential() کامل را دارید!

کد نهایی این بخش

public/client.js

...
export const registerCredential = async () => {
  const opts = {
    attestation: 'none',
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      userVerification: 'required',
      requireResidentKey: false
    }
  };

  const options = await _fetch('/auth/registerRequest', opts);

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }
  
  const cred = await navigator.credentials.create({
    publicKey: options
  });

  const credential = {};
  credential.id =     cred.id;
  credential.rawId =  base64url.encode(cred.rawId);
  credential.type =   cred.type;

  if (cred.response) {
    const clientDataJSON =
      base64url.encode(cred.response.clientDataJSON);
    const attestationObject =
      base64url.encode(cred.response.attestationObject);
    credential.response = {
      clientDataJSON,
      attestationObject
    };
  }

  localStorage.setItem(`credId`, credential.id);
  
  return await _fetch('/auth/registerResponse' , credential);
};
...

4. برای ثبت، دریافت و حذف اعتبارنامه، UI بسازید

داشتن لیستی از اعتبارنامه ها و دکمه های ثبت شده برای حذف آنها خوب است.

9b5b5ae4a7b316bd.png

ایجاد جای‌بان UI

UI را به لیست اعتبارنامه ها و یک دکمه برای ثبت اعتبار جدید اضافه کنید. بسته به در دسترس بودن یا نبودن ویژگی، کلاس hidden را از پیام هشدار یا دکمه ثبت اعتبار جدید حذف می کنید. ul#list جایگاهی برای افزودن لیستی از اعتبارنامه های ثبت شده است.

views/home.html

<p id="uvpa_unavailable" class="hidden">
  This device does not support User Verifying Platform Authenticator. You can't register a credential.
</p>
<h3 class="mdc-typography mdc-typography--headline6">
  Your registered credentials:
</h3>
<section>
  <div id="list"></div>
</section>
<mwc-button id="register" class="hidden" icon="fingerprint" raised>Add a credential</mwc-button>

تشخیص ویژگی و در دسترس بودن UVPA

این مراحل را برای بررسی در دسترس بودن UVPA دنبال کنید:

  1. window.PublicKeyCredential را بررسی کنید تا بررسی کنید که آیا WebAuthn در دسترس است یا خیر.
  2. با PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() تماس بگیرید تا بررسی کنید که آیا UVPA موجود است یا خیر. اگر آنها در دسترس هستند، دکمه ثبت اعتبار جدید را نشان می دهید. اگر یکی از آنها در دسترس نباشد، پیام هشدار را نشان می دهید.

views/home.html

const register = document.querySelector('#register');

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa) {
      register.classList.remove('hidden');
    } else {
      document
        .querySelector('#uvpa_unavailable')
        .classList.remove('hidden');
    }
  });        
} else {
  document
    .querySelector('#uvpa_unavailable')
    .classList.remove('hidden');
}

دریافت و نمایش یک لیست از اعتبار

  1. یک getCredentials() ایجاد کنید تا بتوانید اعتبارنامه های ثبت شده را دریافت کرده و در یک لیست نمایش دهید. خوشبختانه، شما در حال حاضر یک نقطه پایانی مفید در سرور /auth/getKeys دارید که می توانید اعتبارنامه های ثبت شده برای کاربر وارد شده را از آن دریافت کنید.

JSON برگشتی شامل اطلاعات اعتبارنامه، مانند id و publicKey است. می توانید HTML بسازید تا آنها را به کاربر نشان دهید.

views/home.html

const getCredentials = async () => {
  const res = await _fetch('/auth/getKeys');
  const list = document.querySelector('#list');
  const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
    <div class="mdc-card credential">
      <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
      <pre class="public-key">${cred.publicKey}</pre>
      <div class="mdc-card__actions">
        <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
      </div>
    </div>`) : html`
    <p>No credentials found.</p>
    `}`;
  render(creds, list);
};
  1. برای نمایش اعتبارنامه های موجود به محض اینکه کاربر در صفحه /home قرار گرفت، getCredentials() فراخوانی کنید.

views/home.html

getCredentials();

اعتبارنامه را حذف کنید

در لیست اعتبارنامه ها، یک دکمه برای حذف هر اعتبار اضافه کرده اید. برای حذف آنها می توانید درخواستی به /auth/removeKey به همراه پارامتر query credId کنید.

public/client.js

export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
  1. unregisterCredential را به بیانیه import موجود اضافه کنید.

views/home.html

import { _fetch, unregisterCredential } from '/client.js';
  1. هنگامی که کاربر روی Remove کلیک می کند، یک تابع برای تماس اضافه کنید.

views/home.html

const removeCredential = async e => {
  try {
    await unregisterCredential(e.target.id);
    getCredentials();
  } catch (e) {
    alert(e);
  }
};

ثبت اعتبار

هنگامی که کاربر روی Add a credential کلیک می کند، می توانید registerCredential() را برای ثبت یک اعتبار جدید فراخوانی کنید.

  1. registerCredential را به بیانیه import موجود اضافه کنید.

views/home.html

import { _fetch, registerCredential, unregisterCredential } from '/client.js';
  1. registerCredential() را با گزینه هایی برای navigator.credentials.create() فراخوانی کنید.

فراموش نکنید که پس از ثبت نام با فراخوانی getCredentials() لیست اعتبار را تمدید کنید.

views/home.html

register.addEventListener('click', e => {
  registerCredential().then(user => {
    getCredentials();
  }).catch(e => alert(e));
});

اکنون باید بتوانید یک اعتبارنامه جدید ثبت کنید و اطلاعات مربوط به آن را نمایش دهید. می توانید آن را در وب سایت زنده خود امتحان کنید.

کد نهایی این بخش

views/home.html

...
      <p id="uvpa_unavailable" class="hidden">
        This device does not support User Verifying Platform Authenticator. You can't register a credential.
      </p>
      <h3 class="mdc-typography mdc-typography--headline6">
        Your registered credentials:
      </h3>
      <section>
        <div id="list"></div>
        <mwc-fab id="register" class="hidden" icon="add"></mwc-fab>
      </section>
      <mwc-button raised><a href="/reauth">Try reauth</a></mwc-button>
      <mwc-button><a href="/auth/signout">Sign out</a></mwc-button>
    </main>
    <script type="module">
      import { _fetch, registerCredential, unregisterCredential } from '/client.js';
      import { html, render } from 'https://unpkg.com/lit-html@1.0.0/lit-html.js?module';

      const register = document.querySelector('#register');

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa) {
            register.classList.remove('hidden');
          } else {
            document
              .querySelector('#uvpa_unavailable')
              .classList.remove('hidden');
          }
        });        
      } else {
        document
          .querySelector('#uvpa_unavailable')
          .classList.remove('hidden');
      }

      const getCredentials = async () => {
        const res = await _fetch('/auth/getKeys');
        const list = document.querySelector('#list');
        const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
          <div class="mdc-card credential">
            <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
            <pre class="public-key">${cred.publicKey}</pre>
            <div class="mdc-card__actions">
              <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
            </div>
          </div>`) : html`
          <p>No credentials found.</p>
          `}`;
        render(creds, list);
      };

      getCredentials();

      const removeCredential = async e => {
        try {
          await unregisterCredential(e.target.id);
          getCredentials();
        } catch (e) {
          alert(e);
        }
      };

      register.addEventListener('click', e => {
        registerCredential({
          attestation: 'none',
          authenticatorSelection: {
            authenticatorAttachment: 'platform',
            userVerification: 'required',
            requireResidentKey: false
          }
        })
        .then(user => {
          getCredentials();
        })
        .catch(e => alert(e));
      });
    </script>
...

public/client.js

...
export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
...

5. احراز هویت کاربر با اثر انگشت

اکنون یک اعتبار ثبت شده و آماده استفاده به عنوان راهی برای احراز هویت کاربر دارید. اکنون قابلیت احراز هویت مجدد را به وب سایت اضافه می کنید. در اینجا تجربه کاربر است:

هنگامی که کاربر در صفحه /reauth قرار می گیرد، اگر احراز هویت بیومتریک امکان پذیر باشد، دکمه Authenticate را می بیند. احراز هویت با اثر انگشت (UVPA) زمانی شروع می‌شود که روی Authenticate ضربه می‌زنند، با موفقیت تأیید می‌کنند و سپس در صفحه /home فرود می‌آیند. اگر احراز هویت بیومتریک در دسترس نباشد یا احراز هویت با بیومتریک ناموفق باشد، رابط کاربری مجدداً از فرم رمز عبور موجود استفاده می‌کند.

b8770c4e7475b075.png

ایجاد تابع authenticate()

یک تابع به نام authenticate() ایجاد کنید که هویت کاربر را با اثر انگشت تأیید می کند. شما کد جاوا اسکریپت را در اینجا اضافه می کنید:

public/client.js

export const authenticate = async () => {

};

چالش و گزینه های دیگر را از نقطه پایانی سرور دریافت کنید

  1. قبل از احراز هویت، بررسی کنید که آیا کاربر یک شناسه اعتبار ذخیره شده دارد یا خیر و در صورت داشتن آن، آن را به عنوان پارامتر جستجو تنظیم کنید.

هنگامی که یک شناسه اعتبار را همراه با گزینه های دیگر ارائه می کنید، سرور می تواند allowCredentials مربوطه را ارائه دهد و این امر تأیید کاربر را قابل اعتماد می کند.

public/client.js

const opts = {};

let url = '/auth/signinRequest';
const credId = localStorage.getItem(`credId`);
if (credId) {
  url += `?credId=${encodeURIComponent(credId)}`;
}
  1. قبل از اینکه از کاربر بخواهید احراز هویت کند، از سرور بخواهید یک چالش و سایر پارامترها را پس بگیرد. _fetch() را با opts به عنوان آرگومان برای ارسال درخواست POST به سرور فراخوانی کنید.

public/client.js

const options = await _fetch(url, opts);

در اینجا نمونه‌هایی از گزینه‌هایی هستند که باید دریافت کنید (همتراز با PublicKeyCredentialRequestOptions ).

{
  "challenge": "...",
  "timeout": 1800000,
  "rpId": "webauthn-codelab.glitch.me",
  "userVerification": "required",
  "allowCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ]
}

مهمترین گزینه در اینجا allowCredentials است. هنگامی که گزینه‌هایی را از سرور دریافت می‌کنید، بسته به اینکه اعتباری با شناسه در پارامتر query در سمت سرور پیدا شود، allowCredentials باید یا یک شی واحد در یک آرایه یا یک آرایه خالی باشد.

  1. زمانی که allowCredentials یک آرایه خالی است، وعده را با null حل کنید تا UI دوباره به درخواست رمز عبور بازگردد.
if (options.allowCredentials.length === 0) {
  console.info('No registered credentials found.');
  return Promise.resolve(null);
}

کاربر را به صورت محلی تأیید کنید و یک اعتبار دریافت کنید

  1. از آنجایی که این گزینه‌ها به‌منظور عبور از پروتکل HTTP به صورت رمزگذاری شده تحویل داده می‌شوند، برخی از پارامترها را به باینری، به‌ویژه challenge و نمونه‌هایی از id موجود در آرایه allowCredentials ، برگردانید:

public/client.js

options.challenge = base64url.decode(options.challenge);

for (let cred of options.allowCredentials) {
  cred.id = base64url.decode(cred.id);
}
  1. برای تأیید هویت کاربر با یک UVPA، متد navigator.credentials.get() را فراخوانی کنید.

public/client.js

const cred = await navigator.credentials.get({
  publicKey: options
});

هنگامی که کاربر هویت خود را تأیید کرد، باید یک شی اعتبار دریافت کنید که می توانید آن را به سرور ارسال کنید و کاربر را احراز هویت کنید.

تأیید اعتبار

در اینجا یک نمونه شی PublicKeyCredential ( response AuthenticatorAssertionResponse است) که باید دریافت می کردید آمده است:

{
  "id": "...",
  "type": "public-key",
  "rawId": "...",
  "response": {
    "clientDataJSON": "...",
    "authenticatorData": "...",
    "signature": "...",
    "userHandle": ""
  }
}
  1. پارامترهای باینری اعتبارنامه را رمزگذاری کنید تا بتوان آن را به عنوان یک رشته به سرور تحویل داد:

public/client.js

const credential = {};
credential.id = cred.id;
credential.type = cred.type;
credential.rawId = base64url.encode(cred.rawId);

if (cred.response) {
  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,
  };
}
  1. شی را به سرور ارسال کنید و اگر HTTP code 200 ، کاربر را با موفقیت وارد شده در نظر بگیرید:

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

اکنون تابع authentication() کامل را دارید!

کد نهایی این بخش

public/client.js

...
export const authenticate = async () => {
  const opts = {};

  let url = '/auth/signinRequest';
  const credId = localStorage.getItem(`credId`);
  if (credId) {
    url += `?credId=${encodeURIComponent(credId)}`;
  }
  
  const options = await _fetch(url, opts);
  
  if (options.allowCredentials.length === 0) {
    console.info('No registered credentials found.');
    return Promise.resolve(null);
  }

  options.challenge = base64url.decode(options.challenge);

  for (let cred of options.allowCredentials) {
    cred.id = base64url.decode(cred.id);
  }

  const cred = await navigator.credentials.get({
    publicKey: options
  });

  const credential = {};
  credential.id = cred.id;
  credential.type = cred.type;
  credential.rawId = base64url.encode(cred.rawId);

  if (cred.response) {
    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. تجربه احراز هویت مجدد را فعال کنید

ساخت UI

وقتی کاربر برمی گردد، می خواهید تا حد امکان به راحتی و ایمن احراز هویت مجدد انجام شود. اینجاست که احراز هویت بیومتریک می درخشد. با این حال، مواردی وجود دارد که احراز هویت بیومتریک ممکن است کار نکند:

  • UVPA در دسترس نیست.
  • کاربر هنوز هیچ اعتباری را در دستگاه خود ثبت نکرده است.
  • فضای ذخیره سازی پاک می شود و دستگاه دیگر شناسه اعتبار را به خاطر نمی آورد.
  • کاربر به دلایلی نمی تواند هویت خود را تأیید کند، مانند زمانی که انگشتش خیس است یا ماسک زده است.

به همین دلیل است که همیشه مهم است که سایر گزینه‌های ورود به سیستم را به عنوان جایگزین ارائه کنید. در این کد لبه، شما از راه حل رمز مبتنی بر فرم استفاده می کنید.

19da999b0145054.png

  1. برای نمایش یک دکمه احراز هویت که علاوه بر فرم رمز عبور، احراز هویت بیومتریک را نیز فراخوانی می کند، رابط کاربری را اضافه کنید.

از کلاس hidden برای نمایش انتخابی و پنهان کردن یکی از آنها بسته به وضعیت کاربر استفاده کنید.

views/reauth.html

<div id="uvpa_available" class="hidden">
  <h2>
    Verify your identity
  </h2>
  <div>
    <mwc-button id="reauth" raised>Authenticate</mwc-button>
  </div>
  <div>
    <mwc-button id="cancel">Sign-in with password</mwc-button>
  </div>
</div>
  1. class="hidden" را به فرم اضافه کنید:

views/reauth.html

<form id="form" method="POST" action="/auth/password" class="hidden">

تشخیص ویژگی و در دسترس بودن UVPA

در صورت رعایت یکی از این شرایط، کاربران باید با رمز ورود وارد شوند:

  • WebAuthn در دسترس نیست.
  • UVPA در دسترس نیست.
  • شناسه اعتبار برای این UVPA قابل کشف نیست.

به طور انتخابی دکمه احراز هویت را نشان دهید یا آن را پنهان کنید:

views/reauth.html

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa && localStorage.getItem(`credId`)) {
      document
        .querySelector('#uvpa_available')
        .classList.remove('hidden');
    } else {
      form.classList.remove('hidden');
    }
  });        
} else {
  form.classList.remove('hidden');
}

بازگشت به فرم رمز عبور

کاربر همچنین باید بتواند با رمز ورود به سیستم وارد شود.

هنگامی که کاربر روی Sign in with password کلیک می کند، فرم رمز عبور را نشان داده و دکمه احراز هویت را مخفی کنید.

views/reauth.html

const cancel = document.querySelector('#cancel');
cancel.addEventListener('click', e => {
  form.classList.remove('hidden');
  document
    .querySelector('#uvpa_available')
    .classList.add('hidden');
});

c4a82800889f078c.png

احراز هویت بیومتریک را فراخوانی کنید

در نهایت، احراز هویت بیومتریک را فعال کنید.

  1. authenticate را به عبارت import موجود اضافه کنید:

views/reauth.html

import { _fetch, authenticate } from '/client.js';
  1. هنگامی که کاربر برای شروع احراز هویت بیومتریک روی Authenticate ضربه می‌زند authenticate() را فراخوانی کنید.

مطمئن شوید که نقص در احراز هویت بیومتریک به فرم رمز عبور باز می گردد.

views/reauth.html

const button = document.querySelector('#reauth');
button.addEventListener('click', e => {
  authenticate().then(user => {
    if (user) {
      location.href = '/home';
    } else {
      throw 'User not found.';
    }
  }).catch(e => {
    console.error(e.message || e);
    alert('Authentication failed. Use password to sign-in.');
    form.classList.remove('hidden');
    document.querySelector('#uvpa_available').classList.add('hidden');
  });        
});

کد نهایی این بخش

views/reauth.html

...
    <main class="content">
      <div id="uvpa_available" class="hidden">
        <h2>
          Verify your identity
        </h2>
        <div>
          <mwc-button id="reauth" raised>Authenticate</mwc-button>
        </div>
        <div>
          <mwc-button id="cancel">Sign-in with password</mwc-button>
        </div>
      </div>
      <form id="form" method="POST" action="/auth/password" class="hidden">
        <h2>
          Enter a password
        </h2>
        <input type="hidden" name="username" value="{{username}}" />
        <div class="mdc-text-field mdc-text-field--filled">
          <span class="mdc-text-field__ripple"></span>
          <label class="mdc-floating-label" id="password-label">password</label>
          <input type="password" class="mdc-text-field__input" aria-labelledby="password-label" name="password" />
          <span class="mdc-line-ripple"></span>
        </div>
        <input type="submit" class="mdc-button mdc-button--raised" value="Sign-In" />
        <p class="instructions">password will be ignored in this demo.</p>
      </form>
    </main>
    <script src="https://unpkg.com/material-components-web@7.0.0/dist/material-components-web.min.js"></script>
    <script type="module">
      new mdc.textField.MDCTextField(document.querySelector('.mdc-text-field'));
      import { _fetch, authenticate } from '/client.js';
      const form = document.querySelector('#form');
      form.addEventListener('submit', e => {
        e.preventDefault();
        const form = new FormData(e.target);
        const cred = {};
        form.forEach((v, k) => cred[k] = v);
        _fetch(e.target.action, cred)
        .then(user => {
          location.href = '/home';
        })
        .catch(e => alert(e));
      });

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa && localStorage.getItem(`credId`)) {
            document
              .querySelector('#uvpa_available')
              .classList.remove('hidden');
          } else {
            form.classList.remove('hidden');
          }
        });        
      } else {
        form.classList.remove('hidden');
      }

      const cancel = document.querySelector('#cancel');
      cancel.addEventListener('click', e => {
        form.classList.remove('hidden');
        document
          .querySelector('#uvpa_available')
          .classList.add('hidden');
      });

      const button = document.querySelector('#reauth');
      button.addEventListener('click', e => {
        authenticate().then(user => {
          if (user) {
            location.href = '/home';
          } else {
            throw 'User not found.';
          }
        }).catch(e => {
          console.error(e.message || e);
          alert('Authentication failed. Use password to sign-in.');
          form.classList.remove('hidden');
          document.querySelector('#uvpa_available').classList.add('hidden');
        });        
      });
    </script>
...

7. تبریک می گویم!

شما این کد لبه را تمام کردید!

بیشتر بدانید

تشکر ویژه از Yuriy Ackermann از FIDO Alliance برای کمک شما.