Quản lý khoá truy cập

Hướng dẫn này trình bày chi tiết cách cho phép người dùng xem, đổi tên và xoá khoá truy cập đã đăng ký, đồng thời đồng bộ hoá thông tin đăng nhập đã lưu và thông tin hồ sơ người dùng giữa máy chủ và trình quản lý mật khẩu của người dùng bằng Signal API.

Thao tác phía máy chủ

Lớp cơ sở dữ liệu phụ trợ và điểm cuối của bạn PHẢI hỗ trợ các thao tác CRUD phổ biến cho thông tin đăng nhập đã đăng ký cũng như cập nhật thông tin hồ sơ người dùng. Được tách riêng khỏi các thư viện dành riêng cho khung, máy chủ sẽ hiển thị các điểm cuối để:

  1. Liệt kê tất cả thông tin đăng nhập của người dùng: Tìm nạp tất cả bản ghi StoredPasskeyCredential khớp với mã nhận dạng của người dùng đã đăng nhập.
  2. Cập nhật tên thông tin xác thực: Chấp nhận biệt hiệu tuỳ chỉnh mới cho một mã thông tin xác thực cụ thể và duy trì nội dung cập nhật trên máy chủ.
  3. Xoá thông tin xác thực: Xoá một mã thông tin xác thực cụ thể khỏi cơ sở dữ liệu.
  4. Cập nhật thông tin chi tiết về tài khoản người dùng: Chấp nhận các giá trị name (tên người dùng) và displayName đã cập nhật cho hồ sơ của người dùng đã đăng nhập để ứng dụng có thể đồng bộ hoá các giá trị đó với nhà cung cấp khoá truy cập.
// Node.js routing example for credential CRUD and user profile rename
router.get('/api/credentials', checkUserAuthenticated, async (req, res) => {
  const list = await db.findCredentialsByUserId(req.user.id);
  return res.json(list);
});

router.put('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const { name } = req.body;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  cred.name = name;
  await db.saveCredential(cred);
  return res.json(cred);
});

router.delete('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  await db.deleteCredential(id);
  return res.json({ success: true });
});

router.put('/api/user', checkUserAuthenticated, async (req, res) => {
  const { name, displayName } = req.body;
  const updatedUser = await db.updateUser(req.user.id, { name, displayName });
  return res.json(updatedUser);
});

Giao diện người dùng quản lý phía máy khách

Hiển thị một bảng điều khiển chế độ cài đặt riêng, cho phép người dùng dễ dàng kiểm tra và quản lý các lựa chọn xác thực đã đăng ký và thông tin chi tiết về hồ sơ tài khoản:

  1. Hiển thị danh sách đã lưu: Tìm nạp danh sách từ điểm cuối và hiển thị từng hàng thông tin xác thực. Nếu phản hồi trống, hãy hiển thị thông báo trạng thái trống hữu ích (ví dụ: "Không tìm thấy khoá truy cập").
  2. Ánh xạ siêu dữ liệu AAGUID: Đối với mỗi khoá truy cập, hãy tra cứu thuộc tính aaguid của khoá đó trong sổ đăng ký cục bộ để hiển thị thông tin chi tiết về nhà cung cấp. Hãy xem phần Xác định nhà cung cấp khoá truy cập từ AAGUID để biết thêm thông tin chi tiết.
  3. Yêu cầu về giao diện người dùng cho mỗi mục: Mỗi hàng bên trong vùng chứa danh sách PHẢI hiển thị:
    • Biểu tượng nhà cung cấp: Hình ảnh hoặc URI dữ liệu bắt nguồn từ AAGUID.
    • Nhà cung cấp/Tên tuỳ chỉnh: Tên bắt nguồn từ AAGUID hoặc chuỗi do người dùng đổi tên.
    • Registration Date (Ngày đăng ký): Dấu thời gian bắt đầu thô được lưu trữ trong cơ sở dữ liệu registeredAt được định dạng thành ngày mà con người có thể đọc được để hiển thị cho ứng dụng.
    • Ngày sử dụng gần đây nhất: Dấu thời gian bắt đầu kỷ nguyên thô được lưu trữ trong cơ sở dữ liệu lastUsedAt được định dạng thành ngày mà con người có thể đọc được (nếu có) để hiển thị cho ứng dụng.
    • Nút đổi tên: Kích hoạt thao tác đổi tên để cập nhật biệt hiệu tuỳ chỉnh của thông tin xác thực trên máy chủ (PUT /api/credential/:id). Lưu ý: Việc đổi tên biệt hiệu của một khoá truy cập riêng lẻ là thay đổi nhãn phía máy chủ và KHÔNG gọi signalCurrentUserDetails().
    • Nút xoá: Kích hoạt thao tác xoá thông tin xác thực (DELETE /api/credential/:id) và đồng bộ hoá các mã thông tin xác thực còn lại thông qua signalAllAcceptedCredentials().
  4. Giao diện người dùng đổi tên tài khoản người dùng:
    • Cung cấp một chế độ kiểm soát cho phép người dùng cập nhật tên người dùng của tài khoản (name) hoặc displayName (PUT /api/user) và đồng bộ hoá ngay thông tin chi tiết đã cập nhật của người dùng với trình quản lý mật khẩu thông qua PublicKeyCredential.signalCurrentUserDetails().
  5. Nút "Tạo khoá truy cập" có điều kiện:
    • Cung cấp nút kích hoạt đăng ký "Tạo khoá truy cập" nổi bật trên trang quản lý. Trước khi hiển thị phần tử trên giao diện người dùng này, trang PHẢI phát hiện khả năng bằng cách sử dụng PublicKeyCredential.getClientCapabilities() để xác minh rằng trình xác thực nền tảng được hỗ trợ. Nếu khoá truy cập không được hỗ trợ, hãy ẩn nút này và khuyến khích người dùng đăng ký MFA tiêu chuẩn.
    • Cho phép đăng ký khoá bảo mật bằng cách bỏ qua authenticatorSelection.authenticatorAttachment trong lệnh gọi navigator.credentials.create().

Đồng bộ hoá API tín hiệu

Signal API cho phép ứng dụng thông báo trạng thái thông tin xác thực cho trình quản lý mật khẩu, giúp kho lưu trữ đã đồng bộ hoá của người dùng và cơ sở dữ liệu phụ trợ của bạn luôn đồng bộ.

  • Quy tắc phát hiện tính năng và mã hoá tham số:
    • Luôn phát hiện tính năng của các phương thức Signal API trước khi gọi (if (PublicKeyCredential.signalAllAcceptedCredentials) và if (PublicKeyCredential.signalCurrentUserDetails)).
    • Tất cả tham số userId và mã thông tin xác thực được truyền đến các phương thức Signal API (signalAllAcceptedCredentials, signalCurrentUserDetails) PHẢI là chuỗi được mã hoá Base64URL.
  • Bắt đầu đồng bộ hoá khi tải trang:
    • Ứng dụng PHẢI tự động gọi signalAllAcceptedCredentials() khi trang quản lý tải (ví dụ: bên trong trình nghe sự kiện DOMContentLoaded, khởi chạy mô-đun hoặc hook gắn thành phần khung như useEffect).
  • Đồng bộ hoá thông tin cập nhật về quản lý:
    • Ứng dụng PHẢI gọi signalAllAcceptedCredentials() ngay lập tức trong trình xử lý lượt nhấp xoá thông tin xác thực sau khi tìm nạp.
    • Ứng dụng PHẢI gọi signalCurrentUserDetails() ngay lập tức trong trình xử lý lượt nhấp đổi tên tài khoản người dùng (name hoặc displayName) sau khi tìm nạp.
// Client-side management synchronization ES module
import { listFetch, renameCredentialFetch, updateUserFetch, deleteFetch } from './api.js';

// Base64URL-encoded User ID string (illustration only)
const base64UrlUserId = "M2YPl-KGnA8";

async function syncAcceptedCredentials(currentCredentialsList) {
  if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) return;
  try {
    const credentialIds = currentCredentialsList.map(c => c.id); // Array of Base64URL credential ID strings
    
    await PublicKeyCredential.signalAllAcceptedCredentials({
      rpId, // RP ID must match the one defined on the server
      userId: base64UrlUserId, // User ID Base64URL-encoded string
      allAcceptedCredentialIds: credentialIds
    });
  } catch (e) {
    console.error('SignalAllAcceptedCredentials sync failure:', e);
  }
}

async function loadManagementPanel() {
  const response = await listFetch();
  const list = await response.json();
  
  renderUI(list);
  // Sync accepted credentials on initial load
  await syncAcceptedCredentials(list);
}

async function performDelete(credentialId) {
  const response = await deleteFetch(credentialId);
  if (response.ok) {
    const updatedResponse = await listFetch();
    const updatedList = await updatedResponse.json();
    
    renderUI(updatedList);
    // Sync remaining accepted credentials after deletion
    await syncAcceptedCredentials(updatedList);
  }
}

// Renaming a passkey's nickname updates the server record only
async function performCredentialRename(credentialId, newCredentialName) {
  const response = await renameCredentialFetch(credentialId, { name: newCredentialName });
  if (response.ok) {
    const updatedResponse = await listFetch();
    renderUI(await updatedResponse.json());
  }
}

// Renaming the user's account username or displayName triggers signalCurrentUserDetails
async function performUserRename(rpId, userId, updatedName, updatedDisplayName) {
  const response = await updateUserFetch({ name: updatedName, displayName: updatedDisplayName });
  if (response.ok && window.PublicKeyCredential && PublicKeyCredential.signalCurrentUserDetails) {
    try {
      await PublicKeyCredential.signalCurrentUserDetails({
        rpId, // RP ID must match the one defined on the server
        userId, // Base64URL-encoded user ID
        name: updatedName, // Updated username
        displayName: updatedDisplayName // Updated display name
      });
    } catch (e) {
      console.error('SignalCurrentUserDetails sync failure:', e);
    }
  }
}

window.addEventListener('DOMContentLoaded', loadManagementPanel);

Xác định nhà cung cấp khoá truy cập từ AAGUID

AAGUID (Mã nhận dạng duy nhất trên toàn cầu của chứng thực trình xác thực) là một giá trị nhận dạng riêng biệt 128 bit đại diện cho mẫu trình xác thực, chứ không phải một thực thể cụ thể. AAGUID có trong dữ liệu của trình xác thực trong quá trình đăng ký khoá truy cập và có thể dùng để xác định nhà cung cấp khoá truy cập nào (ví dụ: Trình quản lý mật khẩu của Google, Chuỗi khoá iCloud, 1Password) đã tạo thông tin xác thực.

Bạn chỉ nên dùng AAGUID để giúp người dùng quản lý khoá truy cập. Bạn có thể sửa đổi khoá truy cập trừ phi khoá truy cập đó được chứng thực bằng mật mã, điều mà khoá truy cập nền tảng hiện không hỗ trợ.

1. Sổ đăng ký AAGUID

Bạn có thể xem thông tin liên kết JSON do cộng đồng duy trì giữa AAGUID với tên và biểu tượng của nhà cung cấp tại:

https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json

Mỗi mục nhập có giản đồ sau:

{
  "<aaguid-uuid>": {
    "name": "Provider Name",
    "icon_light": "data:image/png;base64,...",
    "icon_dark": "data:image/png;base64,..."
  }
}

2. Sử dụng AAGUID sau khi đăng ký

Sau khi xác minh phản hồi đăng ký, hãy đọc aaguid từ kết quả đăng ký và tra cứu trong sổ đăng ký để điền name và providerIcon của thông tin xác thực:

Trước khi tra cứu AAGUID trong sổ đăng ký, hãy kiểm tra xem giá trị này có bằng '00000000-0000-0000-0000-000000000000' hay không. Nếu có, hãy bỏ qua bước tra cứu sổ đăng ký và đặt name thành một giá trị dự phòng (ví dụ: tên thiết bị từ tác nhân người dùng hoặc "Nhà cung cấp khoá truy cập không xác định") và đặt providerIcon thành undefined. Chỉ tra cứu sổ đăng ký cho các AAGUID khác 0.

import aaguids from './aaguids.json' with { type: 'json' };

const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
  // use the device name as the passkey provider based on
  // the information derived from the user agent string,
  // or just say "Unknown passkey provider"
} else {
  const provider = aaguids[aaguid];
  const credential = {
    // ...other fields
    aaguid,
    name: provider?.name || 'Unknown passkey provider',
    providerIcon: provider?.icon_light,
  };
}

Chiến lược dự phòng

Dự phòng phát hiện tính năng khoá truy cập

Luôn cài đặt "webauthn-polyfills" và nhập vào ngữ cảnh. Hãy cân nhắc rằng miễn là PublicKeyCredential được hỗ trợ, thì PublicKeyCredential.getClientCapabilities cũng được hỗ trợ.

import 'webauthn-polyfills';

Dự phòng đồng bộ hoá API tín hiệu

Nếu trình duyệt không hỗ trợ PublicKeyCredential.parseRequestOptionsFromJSON, hãy sử dụng "webauthn-polyfills":

<script type="module"> 
  if (!PublicKeyCredential.parseRequestOptionsFromJSON) { 
     await import('https://unpkg.com/webauthn-polyfills'); 
   } 
 </script> 

Thao tác này cũng sẽ thêm tính năng hỗ trợ cho PublicKeyCredential.prototype.toJSON.