Xây dựng ứng dụng WebAuthn đầu tiên của bạn

1. Trước khi bắt đầu

API xác thực web (còn gọi là WebAuthn) cho phép bạn tạo và sử dụng thông tin đăng nhập khóa công khai ở phạm vi gốc để xác thực người dùng.

API hỗ trợ việc sử dụng trình xác thực BLE, NFC và U2F hoặc FIDO2 – còn được gọi là khóa bảo mật – cũng như trình xác thực nền tảng, cho phép người dùng xác thực bằng vân tay hoặc khóa màn hình của họ.

Trong lớp học lập trình này, bạn xây dựng một trang web có chức năng xác thực lại đơn giản, sử dụng cảm biến vân tay. Quy trình xác thực lại bảo vệ dữ liệu tài khoản vì quy trình này yêu cầu người dùng đã đăng nhập vào một trang web phải xác thực lại khi họ cố gắng nhập các mục quan trọng trên trang web hoặc truy cập lại vào trang web sau một khoảng thời gian nhất định.

Điều kiện tiên quyết

  • Hiểu cơ bản về cách hoạt động của WebAuthn
  • Kỹ năng lập trình cơ bản bằng JavaScript

Bạn sẽ thực hiện

  • Xây dựng một trang web có chức năng xác thực lại đơn giản, sử dụng cảm biến vân tay

Bạn cần có

  • Một trong những thiết bị sau:
    • Một thiết bị Android, tốt nhất là có cảm biến sinh trắc học
    • iPhone hoặc iPad có Touch ID hoặc Face ID trên iOS 14 trở lên
    • MacBook Pro hoặc Air có Touch ID trên macOS Big Sur trở lên
    • Windows 10 19H1 trở lên khi thiết lập Windows Hello
  • Một trong các trình duyệt sau:
    • Google Chrome 67 trở lên
    • Microsoft Edge 85 trở lên
    • Safari 14 trở lên

2. Bắt đầu thiết lập

Trong lớp học lập trình này, bạn sử dụng dịch vụ có tên là glgl. Đây là nơi bạn có thể chỉnh sửa mã phía máy khách và phía máy chủ bằng JavaScript, đồng thời triển khai các mã đó ngay lập tức.

Truy cập vào https://glching.com/editgoogleplay/webauthn-codelab-start.

Xem cách hoạt động

Hãy làm theo các bước sau để xem trạng thái ban đầu của trang web:

  1. Nhấp vào 62bb7a6aac381af8.png Hiện > 3343769d04c09851.png Trong một cửa sổ mới để xem trang web đang hoạt động.
  2. Nhập tên người dùng mà bạn chọn rồi nhấp vào Tiếp theo.
  3. Nhập mật khẩu rồi nhấp vào Đăng nhập.

Mật khẩu bị bỏ qua, nhưng bạn vẫn được xác thực. Bạn sẽ được chuyển đến trang chủ.

  1. Nhấp vào Thử xác thực lại và lặp lại các bước thứ hai, thứ ba và thứ tư.
  2. Nhấp vào Đăng xuất.

Xin lưu ý rằng bạn phải nhập mật khẩu mỗi lần cố gắng đăng nhập. Điều này mô phỏng một người dùng cần xác thực lại trước khi có thể truy cập vào một mục quan trọng trên trang web.

Phối lại mã

  1. Chuyển đến WebAuthn / FIDO2 API Codelab.
  2. Nhấp vào tên của dự án > Remix Project 306122647ce93305.png để phân phối dự án và tiếp tục với phiên bản của riêng bạn tại một URL mới.

8d42bd24f0fd185c.png

3. Đăng ký thông tin xác thực bằng vân tay

Bạn cần đăng ký thông tin xác thực do UVPA tạo, trình xác thực được tích hợp sẵn trong thiết bị và xác minh danh tính của người dùng. Cảm biến này thường được xem là cảm biến vân tay tùy thuộc vào thiết bị của người dùng.

Bạn thêm tính năng này vào trang /home:

260aab9f1a2587a7.png

Tạo hàm registerCredential()

Tạo một hàm registerCredential() để đăng ký một thông tin xác thực mới.

public/client.js

export const registerCredential = async () => {

};

Lấy thách thức và các tùy chọn khác từ điểm cuối của máy chủ

Trước khi yêu cầu người dùng đăng ký một thông tin xác thực mới, hãy yêu cầu máy chủ trả về các tham số trả về trong WebAuthn, bao gồm cả một thử thách. May mắn thay, bạn đã có điểm cuối máy chủ phản hồi với các thông số như vậy.

Thêm mã sau vào registerCredential().

public/client.js

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

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

Giao thức giữa máy chủ và ứng dụng không thuộc đặc tả WebAuthn. Tuy nhiên, lớp học lập trình này được thiết kế để phù hợp với thông số kỹ thuật của WebAuthn và đối tượng JSON mà bạn chuyển đến máy chủ rất giống với PublicKeyCredentialCreationOptions để bạn có thể dùng trực quan. Bảng sau đây chứa các thông số quan trọng mà bạn có thể chuyển đến máy chủ và giải thích chức năng của chúng:

Thông số

Dòng mô tả

attestation

Ưu tiên truyền đạt chứng thực—none, indirect hoặc direct. Hãy chọn none trừ khi bạn cần một chế độ cài đặt.

excludeCredentials

Mảng PublicKeyCredentialDescriptor để trình xác thực có thể tránh tạo các bản trùng lặp.

authenticatorSelection

authenticatorAttachment

Lọc các trình xác thực có sẵn. Nếu bạn muốn xác thực đi kèm với thiết bị, hãy sử dụng "platform". Đối với trình xác thực chuyển vùng, hãy sử dụng "cross-platform"

userVerification

Xác định xem liệu quá trình xác minh người dùng cục bộ của trình xác thực có phải là "required", "preferred" hoặc "discouraged". Nếu bạn muốn xác thực bằng vân tay hoặc phương thức khóa màn hình, hãy sử dụng "required&quot.

requireResidentKey

Sử dụng true nếu thông tin đăng nhập đã tạo sẽ có sẵn cho trải nghiệm người dùng trong bộ chọn tài khoản trong tương lai.

Để tìm hiểu thêm về các tùy chọn này, hãy xem 5.4. Tùy chọn tạo thông tin xác thực (từ điển PublicKeyCredentialCreationOptions).

Dưới đây là các tùy chọn mẫu mà bạn nhận được từ máy chủ.

{
  "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"
  }
}

Tạo thông tin đăng nhập

  1. Vì các tùy chọn này được phân phối dưới dạng mã hóa để chuyển qua giao thức HTTP, nên hãy chuyển đổi một số thông số trở lại dạng nhị phân, cụ thể là user.id, challenge và các bản sao của id có trong mảng 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. Gọi phương thức navigator.credentials.create() để tạo thông tin xác thực mới.

Với cuộc gọi này, trình duyệt tương tác với trình xác thực và cố gắng xác minh danh tính người dùng với UVPA.

public/client.js

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

Sau khi người dùng xác minh danh tính của họ, bạn sẽ nhận được đối tượng thông tin xác thực mà bạn có thể gửi đến máy chủ và đăng ký trình xác thực.

Đăng ký thông tin xác thực cho điểm cuối của máy chủ

Đây là một đối tượng thông tin xác thực mẫu mà bạn lẽ ra đã nhận được.

{
  "id": "...",
  "rawId": "...",
  "type": "public-key",
  "response": {
    "clientDataJSON": "...",
    "attestationObject": "..."
  }
}
  1. Giống như khi bạn nhận được đối tượng tùy chọn để đăng ký thông tin xác thực, hãy mã hóa các thông số nhị phân của thông tin xác thực để có thể gửi tới máy chủ dưới dạng chuỗi:

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. Lưu trữ mã thông tin đăng nhập cục bộ để bạn có thể sử dụng trong quá trình xác thực khi người dùng quay lại:

public/client.js

localStorage.setItem(`credId`, credential.id);
  1. Gửi đối tượng đến máy chủ và nếu hệ thống trả về HTTP code 200, hãy xem thông tin đăng nhập mới là đã đăng ký thành công.

public/client.js

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

Bây giờ, bạn đã có hàm registerCredential() hoàn chỉnh!

Mã cuối cùng cho mục này

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. Xây dựng giao diện người dùng để đăng ký, nhận và xóa thông tin xác thực

Bạn thật tuyệt khi có danh sách và các nút đã đăng ký để xóa thông tin đăng nhập đó.

9b5b5ae4a7b316bd.png

Xây dựng trình giữ chỗ giao diện người dùng

Thêm giao diện người dùng để liệt kê thông tin xác thực và một nút để đăng ký thông tin xác thực mới. Tùy thuộc vào việc tính năng này có hoạt động hay không, bạn sẽ xóa lớp hidden khỏi thông báo cảnh báo hoặc nút để đăng ký thông tin xác thực mới. ul#list là phần giữ chỗ để thêm danh sách thông tin đăng nhập đã đăng ký.

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>

Phát hiện tính năng và phạm vi hoạt động của tia cực tím

Hãy làm theo các bước sau để kiểm tra khả năng cung cấp UVPA:

  1. Hãy kiểm tra window.PublicKeyCredential để kiểm tra xem WebAuthn có sẵn không.
  2. Gọi PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() để kiểm tra xem có tia cực tím (UVPA) hay không . Nếu họ có sẵn, bạn sẽ hiển thị nút để đăng ký thông tin xác thực mới. Nếu không có một trong hai giá trị này, bạn sẽ thấy thông báo cảnh báo.

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

Lấy và hiển thị danh sách thông tin xác thực

  1. Tạo một hàm getCredentials() để bạn có thể nhận được thông tin đăng nhập và hiển thị các thông tin đó trong một danh sách. May mắn thay, bạn đã có điểm cuối tiện dụng trên máy chủ /auth/getKeys để bạn có thể tìm nạp thông tin đăng nhập đã đăng ký cho người dùng đã đăng nhập.

Tệp JSON được trả về bao gồm các thông tin xác thực, chẳng hạn như idpublicKey. Bạn có thể tạo HTML để hiển thị chúng cho người dùng.

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. Gọi getCredentials() để hiển thị thông tin đăng nhập có sẵn ngay khi người dùng truy cập vào trang /home.

view/home.html

getCredentials();

Xóa thông tin đăng nhập

Trong danh sách thông tin đăng nhập, bạn đã thêm một nút để xóa từng thông tin đăng nhập. Bạn có thể gửi yêu cầu đến /auth/removeKey cùng với thông số truy vấn credId để xóa chúng.

public/client.js

export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
  1. Thêm unregisterCredential vào câu lệnh import hiện có.

view/home.html

import { _fetch, unregisterCredential } from '/client.js';
  1. Thêm một hàm để gọi khi người dùng nhấp vào Xóa.

view/home.html

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

Đăng ký thông tin xác thực

Bạn có thể gọi registerCredential() để đăng ký thông tin xác thực mới khi người dùng nhấp vào Thêm thông tin đăng nhập.

  1. Thêm registerCredential vào câu lệnh import hiện có.

view/home.html

import { _fetch, registerCredential, unregisterCredential } from '/client.js';
  1. Gọi registerCredential() với các tùy chọn cho navigator.credentials.create().

Đừng quên gia hạn danh sách thông tin xác thực bằng cách gọi getCredentials() sau khi đăng ký.

view/home.html

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

Bây giờ, bạn có thể đăng ký một thông tin xác thực mới và hiển thị thông tin về thông tin đó. Bạn có thể kiểm tra trên trang web đang hoạt động của mình.

Mã cuối cùng cho mục này

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. Xác thực người dùng bằng vân tay

Giờ đây, bạn đã đăng ký thông tin đăng nhập và sẵn sàng sử dụng làm phương thức xác thực người dùng. Bây giờ, bạn đã thêm chức năng xác thực lại vào trang web. Đây là trải nghiệm người dùng:

Khi người dùng truy cập vào trang /reauth, họ sẽ thấy nút Xác thực nếu có thể xác thực sinh trắc học. Xác thực bằng vân tay (UVPA) bắt đầu khi họ nhấn vào Xác thực, xác thực thành công rồi truy cập vào trang /home. Nếu không thể sử dụng tính năng xác thực sinh trắc học hoặc xác thực bằng hệ thống nhận dạng sinh trắc học, thì giao diện người dùng sẽ quay lại sử dụng biểu mẫu mật khẩu hiện có.

b8770c4e7475b075.png

Tạo hàm authenticate()

Tạo một hàm có tên là authenticate() để xác minh danh tính của người dùng bằng vân tay. Bạn thêm mã JavaScript tại đây:

public/client.js

export const authenticate = async () => {

};

Lấy thách thức và các tùy chọn khác từ điểm cuối của máy chủ

  1. Trước khi xác thực, hãy kiểm tra xem người dùng có mã thông tin đăng nhập đã lưu trữ hay không và đặt nó làm thông số truy vấn nếu có làm như vậy.

Khi bạn cung cấp mã nhận dạng thông tin xác thực cùng với các tùy chọn khác, máy chủ có thể cung cấp allowCredentials phù hợp và điều này giúp việc xác minh người dùng trở nên đáng tin cậy.

public/client.js

const opts = {};

let url = '/auth/signinRequest';
const credId = localStorage.getItem(`credId`);
if (credId) {
  url += `?credId=${encodeURIComponent(credId)}`;
}
  1. Trước khi bạn yêu cầu người dùng xác thực, hãy yêu cầu máy chủ gửi lại thông tin xác thực và các thông số khác. Gọi _fetch()opts làm đối số để gửi yêu cầu POST tới máy chủ.

public/client.js

const options = await _fetch(url, opts);

Dưới đây là các tùy chọn mẫu mà bạn sẽ nhận được (phù hợp với PublicKeyCredentialRequestOptions).

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

Lựa chọn quan trọng nhất ở đây là allowCredentials. Khi bạn nhận được các tùy chọn từ máy chủ, allowCredentials phải là một đối tượng duy nhất trong một mảng hoặc một mảng trống tùy thuộc vào việc có tìm thấy thông tin đăng nhập có mã nhận dạng trong thông số truy vấn ở phía máy chủ hay không.

  1. Phân giải lời hứa với null khi allowCredentials là một mảng trống để giao diện người dùng quay lại yêu cầu nhập mật khẩu.
if (options.allowCredentials.length === 0) {
  console.info('No registered credentials found.');
  return Promise.resolve(null);
}

Xác minh người dùng cục bộ và nhận thông tin đăng nhập

  1. Vì các tùy chọn này được phân phối dưới dạng mã hóa để thông qua giao thức HTTP, nên hãy chuyển đổi một số thông số trở lại dạng nhị phân, cụ thể là challenge và các bản sao của id có trong mảng allowCredentials:

public/client.js

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

for (let cred of options.allowCredentials) {
  cred.id = base64url.decode(cred.id);
}
  1. Gọi phương thức navigator.credentials.get() để xác minh danh tính của người dùng bằng UVPA.

public/client.js

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

Sau khi người dùng xác minh danh tính của họ, bạn sẽ nhận được đối tượng thông tin xác thực mà bạn có thể gửi đến máy chủ và xác thực người dùng.

Xác minh thông tin đăng nhập

Dưới đây là ví dụ về đối tượng PublicKeyCredential (responseAuthenticatorAssertionResponse) mà bạn lẽ ra đã nhận được:

{
  "id": "...",
  "type": "public-key",
  "rawId": "...",
  "response": {
    "clientDataJSON": "...",
    "authenticatorData": "...",
    "signature": "...",
    "userHandle": ""
  }
}
  1. Mã hóa các thông số nhị phân của thông tin xác thực để có thể phân phối tới máy chủ dưới dạng chuỗi:

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. Gửi đối tượng đến máy chủ và nếu nó trả về HTTP code 200, hãy xem người dùng là đã đăng nhập thành công:

public/client.js

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

Bây giờ, bạn đã có hàm authentication() hoàn chỉnh!

Mã cuối cùng cho mục này

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. Bật chế độ xác thực lại

Xây dựng giao diện người dùng

Khi người dùng quay lại, bạn muốn họ xác thực lại dễ dàng và an toàn nhất có thể. Đây là lúc tính năng xác thực sinh trắc học phát huy hiệu quả. Tuy nhiên, có một số trường hợp mà quá trình xác thực sinh trắc học có thể không hoạt động:

  • Không có UVPA.
  • Người dùng chưa đăng ký thông tin đăng nhập nào trên thiết bị của họ.
  • Bộ nhớ sẽ bị xóa và thiết bị sẽ không còn nhớ mã nhận dạng thông tin xác thực.
  • Người dùng không thể xác minh danh tính của mình vì một lý do nào đó, chẳng hạn như khi ngón tay bị ướt hoặc họ đang đeo khẩu trang.

Đó là lý do tại sao việc cung cấp các tùy chọn đăng nhập khác làm phương án dự phòng luôn quan trọng. Trong lớp học lập trình này, bạn sử dụng giải pháp mật khẩu dựa trên biểu mẫu.

19da999b0145054.png

  1. Thêm giao diện người dùng để hiển thị nút xác thực gọi xác thực sinh trắc học ngoài biểu mẫu mật khẩu.

Dùng lớp hidden để hiển thị và ẩn một trong số các lớp đó tùy thuộc vào trạng thái của người dùng.

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. Thêm class="hidden" vào biểu mẫu:

view/reauth.html

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

Phát hiện tính năng và phạm vi hoạt động của tia cực tím

Người dùng phải đăng nhập bằng mật khẩu nếu một trong các điều kiện sau được đáp ứng:

  • WebAuthn không khả dụng.
  • Không có UVPA.
  • Không thể tìm thấy mã xác thực cho mã UVPA này.

Chọn nút xác thực hoặc ẩn nút xác thực:

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

Biểu mẫu dự phòng cho mật khẩu

Người dùng cũng có thể chọn đăng nhập bằng mật khẩu.

Hiện biểu mẫu mật khẩu và ẩn nút xác thực khi người dùng nhấp vào Đăng nhập bằng mật khẩu:.

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

Gọi phương thức xác thực sinh trắc học

Cuối cùng, hãy bật tính năng xác thực sinh trắc học.

  1. Thêm authenticate vào câu lệnh import hiện có:

view/reauth.html

import { _fetch, authenticate } from '/client.js';
  1. Gọi authenticate() khi người dùng nhấn vào Xác thực để bắt đầu xác thực sinh trắc học.

Đảm bảo rằng lỗi trên quá trình xác thực sinh trắc học quay trở lại biểu mẫu mật khẩu.

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

Mã cuối cùng cho mục này

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. Xin chúc mừng!

Bạn đã hoàn thành lớp học lập trình này!

Tìm hiểu thêm

Đặc biệt, xin cảm ơn Yuriy Ackermann từ FIDO Alliance đã giúp đỡ bạn.