Việc chuyển hướng người dùng chưa xác thực đến một trang đăng nhập riêng khi họ nhấp vào đường liên kết "Đăng nhập", bắt đầu thanh toán, lưu một mục vào danh sách yêu thích hoặc gặp tường phí của bài viết sẽ khiến người dùng mất ngữ cảnh và tăng tỷ lệ bỏ ngang. Chế độ giao diện người dùng tức thì (uiMode: 'immediate' trong navigator.credentials.get()) cho phép đăng nhập nhanh theo ngữ cảnh bằng cách hiển thị ngay một trình chọn tài khoản trình duyệt hợp nhất khi có khoá truy cập cục bộ (publicKey) hoặc mật khẩu đã lưu (password: true) trên thiết bị.
Không giống như lời nhắc WebAuthn phương thức chuẩn (luôn hiển thị giao diện người dùng của trình duyệt và quay lại mã QR trên nhiều thiết bị hoặc khoá bảo mật bên ngoài), chế độ Giao diện người dùng tức thì sẽ nhanh chóng không thực hiện được và trả về NotAllowedError nếu không có thông tin xác thực đủ điều kiện trên thiết bị. Tính năng này cho phép ứng dụng của bạn xác thực người dùng cũ tại chỗ chỉ bằng một lần nhấn, đồng thời chuyển đổi liền mạch người dùng không có thông tin đăng nhập cục bộ sang trang đăng nhập chuẩn, trang thanh toán không cần đăng nhập hoặc biểu mẫu đăng nhập cùng dòng.
Mẫu trải nghiệm người dùng nên dùng và không nên dùng
Kích hoạt chế độ Giao diện người dùng tức thì bên trong trình xử lý sự kiện do người dùng bắt đầu (yêu cầu kích hoạt người dùng tạm thời) tại những thời điểm người dùng có ý định rõ ràng ngay trước khi rời khỏi trang hiện tại. Nếu quá trình xác thực tức thì thành công, hãy hoàn tất ngay ý định của người dùng (bằng cách cập nhật trang hiện tại tại chỗ hoặc chuyển thẳng đến đích đến đã xác thực của họ). Nếu không thành công với NotAllowedError, hãy hiển thị quy trình đăng nhập hoặc đăng ký có đường dẫn để hoàn tất ý định của người dùng khi hoàn tất.
Các trường hợp sử dụng tiềm năng:
- Chặn nút hoặc đường liên kết đăng nhập ở tiêu đề: Chặn lượt nhấp vào đường liên kết "Đăng nhập" chung (
event.preventDefault()) để xác thực người dùng cũ ngay lập tức hoặc tiếp tục chuyển đến trang đăng nhập chuyên biệt trênNotAllowedError. - Đăng nhập linh động trước khi thanh toán: Xác thực người mua sắm cũ bằng một lần nhấn khi họ nhấp vào "Tiến hành thanh toán" để họ chuyển thẳng đến bước vận chuyển và thanh toán, hoặc chuyển sang màn hình đăng nhập tiêu chuẩn hoặc màn hình thanh toán không cần đăng nhập khi dự phòng.
- Đăng nhập cùng dòng cho các hành động nhỏ (Yêu thích, Thích, Đánh dấu, Lưu): Xác thực tại chỗ khi người dùng đã đăng xuất nhấp vào nút hành động nhỏ và hoàn tất hành động ngay lập tức (ví dụ: bật/tắt
aria-pressed="true") hoặc mở một phương thức đăng nhập dự phòng hoặc trang đăng nhập có tham số URL trả về. - Tường phí mềm trong bài viết: Xác thực độc giả tại chỗ khi họ nhấp vào "Tiếp tục đọc bằng tài khoản của bạn" trên biểu ngữ tường phí hoặc mở rộng biểu mẫu đăng nhập hoặc đăng ký cùng dòng trong trường hợp dự phòng.
Các mẫu phản tác dụng cần tránh:
- KHÔNG sử dụng chế độ Giao diện người dùng tức thì trên trang đăng nhập chuyên biệt: Đừng bao giờ đính kèm
uiMode: 'immediate'vào nút "Đăng nhập" hoặc "Đăng nhập bằng khoá truy cập" chính trên trang đăng nhập đích cuối cùng. Người dùng truy cập vào trang đăng nhập chuyên biệt, cụ thể là khi họ không có thông tin xác thực cục bộ trên thiết bị hiện tại hoặc khi họ dự định sử dụng khoá truy cập trên nhiều thiết bị (quét mã QR bằng điện thoại) hoặc khoá bảo mật vật lý bên ngoài. VìuiMode: 'immediate'chặn mã QR trên nhiều thiết bị và các lựa chọn khoá bảo mật, đồng thời ngay lập tức gửiNotAllowedErrorkhi không có thông tin xác thực cục bộ, nên việc sử dụng phương thức này trên một trang đăng nhập chuyên biệt sẽ khiến người dùng rơi vào ngõ cụt, nơi việc nhấp vào nút không có tác dụng gì. Trên trang đăng nhập chuyên biệt, hãy sử dụng Giao diện người dùng có điều kiện (mediation: 'conditional'cóautocomplete="username webauthn") trên dữ liệu đầu vào của biểu mẫu kết hợp với nútnavigator.credentials.get({ publicKey })của phương thức chuẩn (xem phần Xác thực bằng khoá truy cập). - KHÔNG sử dụng chế độ Giao diện người dùng tức thì để xác thực lại từng bước: Tuyệt đối không sử dụng
uiMode: 'immediate'để xác thực lại người dùng đã đăng nhập trước khi thực hiện các thao tác nhạy cảm (chẳng hạn như thay đổi mật khẩu, cập nhật địa chỉ giao hàng, chỉnh sửa phương thức thanh toán đã lưu hoặc thêm khoá truy cập mới). Quá trình xác thực lại phải hạn chế lời nhắc đối với thông tin đăng nhập của người dùng đã đăng nhập thông qua danh sáchallowCredentialskhông trống. Chế độ Giao diện người dùng tức thì sẽ từ chối danh sách này bằngNotAllowedErrorvà việc bỏ quaallowCredentialssẽ khiến người dùng vô tình chọn một tài khoản khác với tài khoản hiện đã đăng nhập. Để xác thực lại theo từng bước, hãy làm theo hướng dẫn trong bài viết Xác thực lại bằng khoá truy cập.
Các quy định ràng buộc về quyền riêng tư và bảo mật
Trình duyệt thực thi các quy tắc nghiêm ngặt về quyền riêng tư và bảo mật khi uiMode: 'immediate' được yêu cầu:
- Cần có hoạt động kích hoạt tạm thời của người dùng: Bạn PHẢI gọi
navigator.credentials.get({ uiMode: 'immediate' })bên trong trình xử lý sự kiện do người dùng bắt đầu (chẳng hạn như trình ngheclicktrên phần tử<button>hoặc<a>) để ngăn chặn việc thăm dò thông tin xác thực thầm lặng. Đừng bao giờ gọi API này tự động khi tải trang (DOMContentLoaded). Việc gọi API không sử dụng hoạt động kích hoạt tạm thời. - Không truyền danh sách
allowCredentialskhông trống: KHÔNG điềnpublicKey.allowCredentialskhiuiMode: 'immediate'được chỉ định. Trình duyệt sẽ từ chối ngay lập tức mọi yêu cầu về chế độ Giao diện người dùng tức thì có danh sáchallowCredentialskhông trống bằng cách gửiNotAllowedErrorđể các bên bị phụ thuộc không thể thăm dò mã thông tin xác thực cụ thể nhằm suy luận các lượt truy cập trước đó hoặc theo dõi người dùng trên các phiên. - Không truyền
AbortSignal(signal): KHÔNG đặt thuộc tínhsignaltrênnavigator.credentials.get()khiuiMode: 'immediate'được chỉ định, vì trình duyệt không cho phép đóng hộp thoại đăng nhập tức thì theo phương thức lập trình. Nếu trang của bạn cũng chạy yêu cầu Giao diện người dùng có điều kiện (mediation: 'conditional') ở chế độ nền, hãy gọiabort()trên Giao diện người dùng có điều kiệnAbortControllerđó trước khi gọi chế độ Giao diện người dùng tức thì và bỏ quasignalkhỏi chính yêu cầu tức thì đó. - Phiên duyệt web ẩn danh và riêng tư: Các yêu cầu được thực hiện trong cửa sổ duyệt web ẩn danh hoặc riêng tư luôn bị từ chối bằng
NotAllowedErrorđể các phiên riêng tư không thể phân biệt được với các phiên không có thông tin xác thực cục bộ.
Thiết lập phía máy chủ
Điểm cuối tạo lựa chọn
Hiển thị một điểm cuối tạo các tuỳ chọn yêu cầu WebAuthn có thể phát hiện cho khách truy cập chưa được xác thực:
- Tạo thử thách liên kết với phiên: Tạo một vùng đệm thử thách ngẫu nhiên bằng mật mã, lưu trữ vùng đệm đó trong phiên máy chủ và mã hoá dưới dạng Base64URL.
- Duy trì lựa chọn ưu tiên về việc xác minh người dùng: Đặt
userVerification: 'preferred'và lưu trữexpectedUserVerification: 'preferred'trong phiên. - Bỏ qua
allowCredentials: Đừng đưa danh sáchallowCredentialskhông trống vào các tuỳ chọn cho chế độ Giao diện người dùng tức thì, vì trình duyệt sẽ từ chối danh sách cho phép không trống cóNotAllowedError.
// Server-side options generation example (/api/login/options)
router.post('/api/login/options', (req, res) => {
const options = {
challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
rpId: 'example.com',
userVerification: 'preferred',
};
req.session.challenge = options.challenge;
req.session.expectedUserVerification = 'preferred';
return res.json(options);
});
Điểm cuối xác minh thông tin xác thực kép
Vì navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) có thể phân giải bằng khoá truy cập (PublicKeyCredential) hoặc mật khẩu đã lưu (PasswordCredential), hãy cung cấp trình xử lý xác minh cho cả hai loại thông tin xác thực:
- Xác minh câu nhận định WebAuthn (
/api/login/verify): Xác minh câu nhận định dựa trên thử thách của phiên và khoá công khai được lưu trữ. Nếu không tìm thấy mã thông tin xác thực trong cơ sở dữ liệu, hãy trả về trạng thái HTTP404để ứng dụng có thể thông báo cho trình quản lý mật khẩu thông quaPublicKeyCredential.signalUnknownCredential(). - Xác minh mật khẩu (
/api/login/password): Xác minhusername(cred.id) vàpassword(cred.password) dựa trên cơ sở dữ liệu người dùng và thiết lập một phiên đã xác thực.
Triển khai phía máy khách
Yêu cầu thông tin xác thực hợp nhất
- Yêu cầu cả khoá truy cập và mật khẩu: Giải mã các tuỳ chọn máy chủ bằng
PublicKeyCredential.parseRequestOptionsFromJSON()và gọinavigator.credentials.get()bên trong trình xử lý lượt nhấp do người dùng bắt đầu bằngpassword: true,publicKeyvàuiMode: 'immediate'(mà không truyềnsignal). - Xác minh chi nhánh muộn nhất vào ngày
cred.type:- Nếu
cred.type === 'public-key', hãy chuyển đổi câu nhận định sang dạng tuần tự bằngcred.toJSON()và POST câu nhận định đó đến điểm cuối xác minh WebAuthn. Khi máy chủ phản hồi bằng HTTP404, hãy gọiPublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }). - Nếu
cred.type === 'password', hãy POST{ username: cred.id, password: cred.password }đến điểm cuối xác minh mật khẩu của bạn.
- Nếu
- Xử lý
NotAllowedErrorkhi không có thông tin xác thực cục bộ: Phát hiệnNotAllowedError(được gửi ngay lập tức khi không có khoá truy cập cục bộ hoặc mật khẩu đã lưu trên thiết bị, hoặc khi người dùng đóng bộ chọn) và chuyển sang quy trình đăng nhập chuẩn (chẳng hạn như mở hộp thoại đăng nhập cùng dòng hoặc chuyển đến/signinbằng URL trả về).
let isAuthenticating = false;
async function authenticateWithImmediateUi() {
const optionsResponse = await fetch('/api/login/options', { method: 'POST' });
if (!optionsResponse.ok) {
throw new Error('Failed to fetch authentication options');
}
const optionsJSON = await optionsResponse.json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);
// MANDATORY: Request both password and publicKey with uiMode: 'immediate' and NO signal parameter
const cred = await navigator.credentials.get({
password: true,
publicKey,
uiMode: 'immediate',
});
if (!cred) {
throw new DOMException('No credential returned', 'NotAllowedError');
}
if (cred.type === 'public-key') {
const encoded = cred.toJSON();
const verifyResponse = await fetch('/api/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(encoded),
});
if (verifyResponse.status === 404) {
await PublicKeyCredential.signalUnknownCredential({
rpId: publicKey.rpId || window.location.hostname,
credentialId: encoded.id, // Base64URL-encoded credential ID string
});
throw new Error('Passkey credential is no longer recognized by the server');
}
if (!verifyResponse.ok) {
throw new Error('Passkey verification failed');
}
return await verifyResponse.json();
}
if (cred.type === 'password') {
const verifyResponse = await fetch('/api/login/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: cred.id,
password: cred.password,
}),
});
if (!verifyResponse.ok) {
throw new Error('Password verification failed');
}
return await verifyResponse.json();
}
throw new Error(`Unsupported credential type: ${cred.type}`);
}
// Example: Intercepting an inline micro-action (Favorite button) or Header Sign-in click
const favoriteButton = document.getElementById('favorite-btn');
favoriteButton.addEventListener('click', async (event) => {
event.preventDefault();
if (isAuthenticating) return;
// If already signed in, toggle the micro-action state immediately
if (document.body.dataset.signedIn === 'true') {
const nextPressed = favoriteButton.getAttribute('aria-pressed') !== 'true';
favoriteButton.setAttribute('aria-pressed', String(nextPressed));
return;
}
isAuthenticating = true;
try {
const user = await authenticateWithImmediateUi();
// Complete the user's intended action immediately
document.body.dataset.signedIn = 'true';
favoriteButton.setAttribute('aria-pressed', 'true');
updateHeaderUserState(user);
} catch (err) {
if (err.name === 'NotAllowedError') {
// No local credentials available or user dismissed dialog:
// transition smoothly to fallback sign-in UI or redirect with return URL
showFallbackSignInUi({ returnAction: 'favorite' });
return;
}
console.error('Authentication error:', err);
} finally {
isAuthenticating = false;
}
});
Chiến lược dự phòng
Dự phòng chế độ giao diện người dùng ngay lập tức
- Trải nghiệm cải tiến tăng dần: Hãy coi
uiMode: 'immediate'là một tính năng cải tiến tăng dần. Đừng suy luận khả năng hỗ trợ từ tên hoặc phiên bản của trình duyệt; tín hiệu hỗ trợ đáng tin cậy duy nhất là quy trình kiểm tra thời gian chạycapabilities.immediateGetthông quaPublicKeyCredential.getClientCapabilities(). BẮT BUỘC: Luôn kiểm tracapabilities.immediateGettrước khi gọinavigator.credentials.get({ uiMode: 'immediate' })– các trình duyệt không nhận dạng đượcuiMode: 'immediate'sẽ bỏ qua khoá từ điển không xác định và mở hộp thoại mã QR và khoá truy cập phương thức chặn. Khicapabilities.immediateGetlà giá trị giả hoặc khinavigator.credentials.get()từ chối bằngNotAllowedError, hãy quay lại trải nghiệm đăng nhập chuẩn của ứng dụng (chẳng hạn như mở một phương thức đăng nhập cùng dòng hoặc chuyển đến trang đăng nhập chuyên dụng có tham số URL trả về) để người dùng trên các trình duyệt không được hỗ trợ thấy quy trình đăng nhập hiện tại của bạn không thay đổi:
async function isImmediateUiSupported() {
if (!window.PublicKeyCredential || !PublicKeyCredential.getClientCapabilities) {
return false;
}
try {
const capabilities = await PublicKeyCredential.getClientCapabilities();
return Boolean(capabilities?.immediateGet);
} catch {
return false;
}
}
async function authenticateWithImmediateUi() {
// MANDATORY: Check immediateGet support so unsupported browsers do not open a modal QR dialog
const supported = await isImmediateUiSupported();
if (!supported) {
// Throw NotAllowedError so the caller's single catch path treats "unsupported" like "no local credentials"
throw new DOMException('Immediate UI mode is not supported', 'NotAllowedError');
}
// Proceed with fetching options and calling navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' })
}
Dự phòng phát hiện chức năng của ứng dụng
Luôn cài đặt 'webauthn-polyfills' và nhập vào mô-đun ứng dụng để PublicKeyCredential.getClientCapabilities() luôn có sẵn bất cứ khi nào PublicKeyCredential được hỗ trợ:
import 'webauthn-polyfills';
Dự phòng thông tin xác thực bằng mật khẩu
Việc truyền password: true cùng với publicKey trong navigator.credentials.get() cho phép các trình duyệt hỗ trợ thêm mật khẩu đã lưu từ Trình quản lý mật khẩu của trình duyệt vào Trình chọn tài khoản tức thì hợp nhất.
- Trải nghiệm dự phòng: Các trình duyệt không hỗ trợ
PasswordCredentialsẽ bỏ qua tuỳ chọnpassword: truehoặc quay lại biểu mẫu đăng nhập chuẩn thông qua quy trình kiểm tra tính năngcapabilities.immediateGet, trong đó<input autocomplete="username webauthn">và<input type="password" autocomplete="current-password">cung cấp tính năng tự động điền mật khẩu gốc.
Dễ dàng dự phòng chuyển đổi tuần tự JSON
Luôn cài đặt 'webauthn-polyfills' và nhập vào gói ứng dụng để PublicKeyCredential.parseRequestOptionsFromJSON() và PublicKeyCredential.prototype.toJSON() hoạt động nhất quán trên các trình duyệt hỗ trợ PublicKeyCredential:
import 'webauthn-polyfills';
Dự phòng đồng bộ hoá Signal API
WebAuthn Signal API (webauthn-signals) là một cải tiến tăng dần giúp xoá các khoá truy cập lỗi thời khỏi Trình quản lý mật khẩu của người dùng khi máy chủ phản hồi bằng HTTP 404.
- Trải nghiệm dự phòng: Chặn cuộc gọi bằng
if (PublicKeyCredential.signalUnknownCredential). Khi không được hỗ trợ, hãy bỏ qua lệnh gọi tín hiệu một cách linh hoạt và hiển thị thông báo rõ ràng hướng dẫn người dùng đăng nhập bằng một phương thức khác.