สร้างแอป WebAuthn แอปแรกของคุณ

1. ข้อควรทราบก่อนที่จะเริ่มต้น

Web Authentication API หรือที่รู้จักกันในชื่อ WebAuthn ช่วยให้คุณสร้างและใช้ข้อมูลเข้าสู่ระบบที่มีขอบเขตระดับสาธารณะในการตรวจสอบสิทธิ์ผู้ใช้

API รองรับการใช้ Authenticator ของ BLE, NFC และ U2F หรือ FIDO2 ที่โรมมิ่งผ่าน USB หรือที่เรียกว่าคีย์ความปลอดภัย รวมถึง Authenticator ของแพลตฟอร์มที่ช่วยให้ผู้ใช้ตรวจสอบสิทธิ์ด้วยลายนิ้วมือหรือการล็อกหน้าจอได้

ใน Codelab นี้ คุณจะสร้างเว็บไซต์ที่มีฟังก์ชันการตรวจสอบสิทธิ์ซ้ําที่เรียบง่ายซึ่งใช้เซ็นเซอร์ลายนิ้วมือ การตรวจสอบสิทธิ์อีกครั้งจะปกป้องข้อมูลบัญชีเนื่องจากผู้ใช้ต้องที่ลงชื่อเข้าใช้เว็บไซต์แล้วต้องตรวจสอบสิทธิ์อีกครั้งเมื่อพยายามป้อนส่วนสําคัญของเว็บไซต์หรือกลับมาเข้าชมเว็บไซต์หลังจากระยะเวลาหนึ่ง

สิ่งที่ต้องมีก่อน

  • ความเข้าใจเบื้องต้นเกี่ยวกับวิธีการทํางานของ WebAuthn
  • ทักษะการเขียนโปรแกรมเบื้องต้นด้วย JavaScript

สิ่งที่คุณจะทํา

  • สร้างเว็บไซต์ที่มีฟังก์ชันการตรวจสอบสิทธิ์ซ้ําที่เรียบง่ายซึ่งใช้เซ็นเซอร์ลายนิ้วมือ

สิ่งที่ต้องมี

  • อุปกรณ์เครื่องใดเครื่องหนึ่งต่อไปนี้:
    • อุปกรณ์ Android ควรมีเซ็นเซอร์ไบโอเมตริก
    • iPhone หรือ iPad ที่ใช้ Touch ID หรือ Face ID ใน iOS 14 ขึ้นไป
    • MacBook Pro หรือ Air ที่มี Touch ID ใน macOS Big Sur หรือสูงกว่า
    • Windows 10 19H1 ขึ้นไปที่มีการตั้งค่า Windows Hello
  • เบราว์เซอร์ใดเบราว์เซอร์หนึ่งต่อไปนี้
    • Google Chrome 67 ขึ้นไป
    • Microsoft Edge 85 ขึ้นไป
    • Safari 14 ขึ้นไป

2. ตั้งค่า

ใน Codelab นี้ คุณจะใช้บริการชื่อ glitch ซึ่งเป็นที่ที่คุณจะแก้ไขโค้ดฝั่งไคลเอ็นต์และฝั่งเซิร์ฟเวอร์ด้วย JavaScript และใช้งานได้ทันที

ไปที่ https://glitch.com/editupload/webauthn-codelab-start

ดูวิธีการทำงาน

ทําตามขั้นตอนต่อไปนี้เพื่อดูสถานะเริ่มต้นของเว็บไซต์

  1. คลิก 62bb7a6aac381af8.png แสดง &gt 3343769d04c09851.png ในหน้าต่างใหม่เพื่อดูเว็บไซต์ที่เผยแพร่อยู่
  2. ป้อนชื่อผู้ใช้ที่ต้องการ แล้วคลิกถัดไป
  3. ป้อนรหัสผ่าน แล้วคลิกลงชื่อเข้าใช้

ระบบจะไม่สนใจรหัสผ่าน แต่คุณยังตรวจสอบสิทธิ์ต่อไป ระบบจะนําคุณไปที่หน้าแรก

  1. คลิกลองทําการตรวจสอบสิทธิ์อีกครั้ง แล้วทําตามขั้นตอนที่ 2, 3 และ 4 ซ้ํา
  2. คลิกออกจากระบบ

โปรดอย่าลืมว่าคุณต้องป้อนรหัสผ่านทุกครั้งที่พยายามลงชื่อเข้าใช้ วิธีนี้จะจําลองผู้ใช้ที่ต้องตรวจสอบสิทธิ์อีกครั้งก่อนที่จะเข้าถึงส่วนสําคัญของเว็บไซต์ได้

รีมิกซ์โค้ด

  1. ไปที่ WebAuthn / FIDO2 API Codelab
  2. คลิกชื่อโปรเจ็กต์ > Remix Project 306122647ce93305.png เพื่อแยกโปรเจ็กต์และดําเนินการต่อในเวอร์ชันของคุณเองที่ URL ใหม่

ไฟล์ 8d42bd24f0fd185c.png

3. ลงทะเบียนข้อมูลรับรองด้วยลายนิ้วมือ

คุณต้องลงทะเบียนข้อมูลรับรองที่สร้างโดย UVPA ซึ่งเป็น Authenticator ที่รวมอยู่ในอุปกรณ์และยืนยันตัวตนของผู้ใช้ ซึ่งโดยปกติจะเห็นเป็นเซ็นเซอร์ลายนิ้วมือ ทั้งนี้ขึ้นอยู่กับอุปกรณ์ของผู้ใช้

คุณเพิ่มฟีเจอร์นี้ลงในหน้า /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 แต่ Codelab นี้ได้รับการออกแบบมาให้สอดคล้องกับข้อกําหนดของ WebAuthn และออบเจ็กต์ JSON ที่คุณส่งไปยังเซิร์ฟเวอร์จะคล้ายกับ PublicKeyCredentialCreationOptions มาก คุณจึงใช้งานได้อย่างง่ายดาย ตารางต่อไปนี้มีพารามิเตอร์สําคัญที่คุณส่งไปยังเซิร์ฟเวอร์และอธิบายหน้าที่ของพารามิเตอร์

พารามิเตอร์

คำอธิบาย

attestation

ค่ากําหนดสําหรับเอกสารรับรอง—none, indirect หรือ direct เลือก none เว้นแต่จําเป็นต้องใช้

excludeCredentials

อาร์เรย์ของ PublicKeyCredentialDescriptor เพื่อให้ Authenticator หลีกเลี่ยงการสร้างรายชื่อที่ซ้ํากัน

authenticatorSelection

authenticatorAttachment

กรอง Authenticator ที่มีอยู่ หากต้องการให้มี Authenticator แนบไว้กับอุปกรณ์ ให้ใช้ "platform" สําหรับ Authenticator แบบโรมมิ่ง ให้ใช้ "cross-platform"

userVerification

ระบุว่าการยืนยันผู้ใช้ Authenticator คือ "required", "preferred" หรือ "discouraged" หากคุณต้องการการตรวจสอบสิทธิ์ด้วยลายนิ้วมือหรือการล็อกหน้าจอ ให้ใช้ "required"

requireResidentKey

ใช้ true หากข้อมูลเข้าสู่ระบบที่สร้างขึ้นควรใช้ได้กับ UX ของเครื่องมือเลือกบัญชีในอนาคต

ดูข้อมูลเพิ่มเติมเกี่ยวกับตัวเลือกเหล่านี้ได้ที่ 5.4 ตัวเลือกสําหรับการสร้างข้อมูลเข้าสู่ระบบ (พจนานุกรม 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,
});

เมื่อผู้ใช้ยืนยันตัวตนแล้ว คุณจะได้รับออบเจ็กต์ข้อมูลเข้าสู่ระบบที่จะส่งไปให้เซิร์ฟเวอร์และลงทะเบียน Authenticator ได้

ลงทะเบียนข้อมูลรับรองไปยังปลายทางเซิร์ฟเวอร์

ตัวอย่างออบเจ็กต์ข้อมูลเข้าสู่ระบบที่คุณควรจะได้รับ

{
  "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 เป็นตัวยึดตําแหน่งสําหรับการเพิ่มรายการข้อมูลรับรองที่ลงทะเบียน

view/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 พร้อมใช้งานหรือไม่ หากมี ให้แสดงปุ่มเพื่อลงทะเบียนข้อมูลรับรองใหม่ โดยจะแสดงข้อความเตือนหากข้อความนั้นไม่มี

view/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 เพื่อแสดงให้ผู้ใช้เห็นได้

view/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. เรียกใช้ getCredentials() เพื่อแสดงข้อมูลเข้าสู่ระบบที่พร้อมใช้งานทันทีที่ผู้ใช้ไปถึงหน้า /home

view/home.html

getCredentials();

นําข้อมูลเข้าสู่ระบบออก

ในรายการข้อมูลเข้าสู่ระบบ คุณต้องเพิ่มปุ่มเพื่อนําข้อมูลรับรองแต่ละรายการออก คุณสามารถส่งคําขอไปยัง /auth/removeKey พร้อมพารามิเตอร์การค้นหา credId เพื่อนําคําขอออกได้

public/client.js

export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
  1. เพิ่ม unregisterCredential ต่อท้ายคําสั่ง import ที่มีอยู่

view/home.html

import { _fetch, unregisterCredential } from '/client.js';
  1. เพิ่มฟังก์ชันในการโทรเมื่อผู้ใช้คลิกนําออก

view/home.html

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

ลงทะเบียนข้อมูลรับรอง

คุณสามารถเรียกใช้ registerCredential() เพื่อลงทะเบียนข้อมูลรับรองใหม่เมื่อผู้ใช้คลิกเพิ่มข้อมูลเข้าสู่ระบบ

  1. เพิ่ม registerCredential ต่อท้ายคําสั่ง import ที่มีอยู่

view/home.html

import { _fetch, registerCredential, unregisterCredential } from '/client.js';
  1. เรียกใช้ registerCredential() ด้วยตัวเลือกสําหรับ navigator.credentials.create()

อย่าลืมต่ออายุรายการข้อมูลเข้าสู่ระบบโดยโทรไปที่ getCredentials() หลังจากลงทะเบียน

view/home.html

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

ตอนนี้คุณควรลงทะเบียนข้อมูลเข้าสู่ระบบใหม่และแสดงข้อมูลเข้าสู่ระบบได้แล้ว ลองใช้งานได้ในเว็บไซต์ที่เผยแพร่อยู่

รหัสสุดท้ายของส่วนนี้

view/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 ผู้ใช้จะเห็นปุ่มตรวจสอบสิทธิ์หากตรวจสอบสิทธิ์ข้อมูลไบโอเมตริกได้ การตรวจสอบสิทธิ์ด้วยลายนิ้วมือ (UVPA) จะเริ่มต้นเมื่อผู้ใช้แตะตรวจสอบสิทธิ์ ตรวจสอบสิทธิ์เรียบร้อยแล้ว แล้วไปที่หน้า /home หากการตรวจสอบสิทธิ์ด้วยข้อมูลไบโอเมตริกไม่พร้อมใช้งานหรือการตรวจสอบสิทธิ์ด้วยข้อมูลไบโอเมตริกไม่สําเร็จ UI จะกลับไปใช้แบบฟอร์มรหัสผ่านที่มีอยู่

b8770c4e7475b075.png

สร้างฟังก์ชัน authenticate()

สร้างฟังก์ชันชื่อ authenticate() ซึ่งจะยืนยันตัวตนของผู้ใช้ด้วยลายนิ้วมือ เพิ่มโค้ด JavaScript ที่นี่

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 เมื่อคุณได้รับตัวเลือกจากเซิร์ฟเวอร์ allowCredentials ควรเป็นออบเจ็กต์เดียวในอาร์เรย์หรืออาร์เรย์ที่ว่างเปล่า โดยขึ้นอยู่กับว่ามีการพบข้อมูลเข้าสู่ระบบที่มีรหัสในพารามิเตอร์การค้นหาในฝั่งเซิร์ฟเวอร์หรือไม่

  1. แก้ไขสัญญากับ null เมื่อ allowCredentials เป็นอาร์เรย์ที่ว่างเปล่าเพื่อให้ 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. เรียกใช้เมธอด navigator.credentials.get() เพื่อยืนยันตัวตนของผู้ใช้ด้วย UVPA

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 ไม่พร้อมใช้งาน
  • ผู้ใช้ยังไม่ได้ลงทะเบียนข้อมูลเข้าสู่ระบบในอุปกรณ์
  • ระบบจะล้างพื้นที่เก็บข้อมูลและอุปกรณ์จะไม่จดจํารหัสข้อมูลเข้าสู่ระบบอีกต่อไป
  • ผู้ใช้ยืนยันตัวตนไม่ได้ด้วยเหตุผลบางอย่าง เช่น เมื่อนิ้วเปียกหรือสวมหน้ากาก

ด้วยเหตุนี้คุณจึงควรให้ตัวเลือกการลงชื่อเข้าใช้อื่นๆ เป็นตัวเลือกสํารอง ใน Codelab นี้ คุณจะใช้โซลูชันรหัสผ่านตามฟอร์ม

19da999b0145054.png

  1. เพิ่ม UI เพื่อแสดงปุ่มการตรวจสอบสิทธิ์ที่เรียกใช้การตรวจสอบสิทธิ์ด้วยข้อมูลไบโอเมตริกนอกเหนือจากแบบฟอร์มรหัสผ่าน

ใช้ชั้นเรียน hidden เพื่อเลือกแสดงและซ่อนคลาสทั้งหมดตามสถานะของผู้ใช้

view/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" ลงในแบบฟอร์ม:

view/reauth.html

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

การตรวจหาฟีเจอร์และความพร้อมใช้งานของ UVPA

ผู้ใช้ต้องลงชื่อเข้าใช้ด้วยรหัสผ่านหากเป็นไปตามเงื่อนไขข้อใดข้อหนึ่งต่อไปนี้

  • WebAuthn ไม่พร้อมใช้งาน
  • UVPA ไม่พร้อมใช้งาน
  • ไม่พบรหัสข้อมูลเข้าสู่ระบบสําหรับ UVPA นี้

เลือกแสดงปุ่มการตรวจสอบสิทธิ์หรือซ่อนไว้ ดังนี้

view/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');
}

เปลี่ยนกลับไปเป็นรูปแบบรหัสผ่าน

ผู้ใช้ควรเลือกที่จะลงชื่อเข้าใช้ด้วยรหัสผ่านได้ด้วย

แสดงแบบฟอร์มรหัสผ่านและซ่อนปุ่มการตรวจสอบสิทธิ์เมื่อผู้ใช้คลิกลงชื่อเข้าใช้ด้วยรหัสผ่าน

view/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 ที่มีอยู่:

view/reauth.html

import { _fetch, authenticate } from '/client.js';
  1. เรียกใช้ authenticate() เมื่อผู้ใช้แตะ ตรวจสอบสิทธิ์ เพื่อเริ่มการตรวจสอบสิทธิ์ด้วยข้อมูลไบโอเมตริก

ตรวจสอบว่าการตรวจสอบสิทธิ์ด้วยข้อมูลไบโอเมตริกไม่กลับไปเป็นแบบฟอร์มรหัสผ่าน

view/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');
  });        
});

รหัสสุดท้ายของส่วนนี้

view/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. ยินดีด้วย

คุณสิ้นสุด Codelab นี้แล้ว

ดูข้อมูลเพิ่มเติม

ขอขอบคุณเป็นพิเศษสําหรับ Yuriy Ackermann จาก FIDO Alliance ที่คอยให้ความช่วยเหลือ