Tổng quan
Sau đây là thông tin tổng quan về các bước chính trong quy trình đăng ký khoá truy cập:

- Xác định các lựa chọn để tạo khoá truy cập. Gửi các tham số này đến ứng dụng để bạn có thể truyền chúng vào lệnh gọi tạo khoá truy cập: lệnh gọi API WebAuthn
navigator.credentials.createtrên web vàcredentialManager.createCredentialtrên Android. Sau khi người dùng xác nhận việc tạo khoá truy cập, lệnh gọi tạo khoá truy cập sẽ được giải quyết và trả về thông tin xác thựcPublicKeyCredential. - Xác minh thông tin đăng nhập và lưu trữ thông tin đó trên máy chủ.
Các phần sau đây sẽ đi sâu vào chi tiết của từng bước.
Tạo các lựa chọn tạo thông tin xác thực
Bước đầu tiên bạn cần thực hiện trên máy chủ là tạo một đối tượng PublicKeyCredentialCreationOptions.
Để làm việc này, hãy dựa vào thư viện phía máy chủ FIDO. Thư viện này thường cung cấp một hàm hiệu dụng có thể tạo các tuỳ chọn này cho bạn. Ví dụ: SimpleWebAuthn cung cấp generateRegistrationOptions.
PublicKeyCredentialCreationOptions phải bao gồm mọi thông tin cần thiết để tạo khoá truy cập: thông tin về người dùng, về bên Relying Party và cấu hình cho các thuộc tính của thông tin xác thực mà bạn đang tạo. Sau khi bạn xác định tất cả các giá trị này, hãy truyền các giá trị đó (nếu cần) đến hàm trong thư viện phía máy chủ FIDO chịu trách nhiệm tạo đối tượng PublicKeyCredentialCreationOptions.
Một số trường của PublicKeyCredentialCreationOptions có thể là hằng số. Các giá trị khác phải được xác định linh động trên máy chủ:
rpId: Để điền mã nhận dạng của bên đáng tin cậy trên máy chủ, hãy sử dụng các hàm hoặc biến phía máy chủ cung cấp cho bạn tên máy chủ của ứng dụng web, chẳng hạn nhưexample.com.user.namevàuser.displayName: Để điền các trường này, hãy sử dụng thông tin phiên của người dùng đã đăng nhập (hoặc thông tin tài khoản người dùng mới, nếu người dùng đang tạo khoá truy cập khi đăng ký).user.namethường là địa chỉ email và là địa chỉ duy nhất cho RP.user.displayNamelà tên thân thiện với người dùng. Xin lưu ý rằng không phải nền tảng nào cũng sử dụngdisplayName.user.id: Một chuỗi ngẫu nhiên, duy nhất được tạo khi tạo tài khoản. Giá trị nhận dạng này phải là vĩnh viễn, không giống như tên người dùng có thể chỉnh sửa được. Mã nhận dạng người dùng giúp xác định một tài khoản, nhưng không được chứa bất kỳ thông tin nhận dạng cá nhân (PII) nào. Có thể bạn đã có mã nhận dạng người dùng trong hệ thống, nhưng nếu cần, hãy tạo một mã nhận dạng dành riêng cho khoá truy cập để đảm bảo không chứa bất kỳ thông tin nhận dạng cá nhân (PII) nào.excludeCredentials: Danh sách mã nhận dạng của thông tin xác thực hiện có để ngăn việc tạo khoá truy cập trùng lặp từ nhà cung cấp khoá truy cập. Để điền vào trường này, hãy tra cứu thông tin xác thực hiện có của người dùng này trong cơ sở dữ liệu của bạn. Xem thông tin chi tiết tại phần Ngăn việc tạo khoá truy cập mới nếu đã có khoá truy cập.challenge: Đối với việc đăng ký thông tin xác thực, thử thách không liên quan trừ phi bạn sử dụng chứng thực, một kỹ thuật nâng cao hơn để xác minh danh tính của nhà cung cấp khoá truy cập và dữ liệu mà nhà cung cấp đó phát ra. Tuy nhiên, ngay cả khi bạn không sử dụng quy trình chứng thực, thì thử thách vẫn là một trường bắt buộc. Hướng dẫn tạo thử thách an toàn để xác thực có trong bài viết Xác thực khoá truy cập phía máy chủ.
Mã hoá và giải mã
PublicKeyCredentialCreationOptions do máy chủ gửi. Các trường nhị phân như challenge và user.id được mã hoá phía máy chủ thành base64URL để các lựa chọn có thể được phân phối dưới dạng JSON qua HTTPS.Đối tượng WebAuthn PublicKeyCredentialCreationOptions chuẩn bao gồm các trường là ArrayBuffer, không thể chuyển đổi tuần tự trực tiếp bằng JSON.stringify(). Để gửi các tuỳ chọn này qua HTTPS, các trường nhị phân phải được mã hoá dưới dạng chuỗi base64URL:
- Trên máy chủ, thư viện phía máy chủ FIDO (chẳng hạn như SimpleWebAuthn) sẽ tự động tạo một đối tượng tuỳ chọn có thể chuyển đổi tuần tự JSON (ví dụ:
PublicKeyCredentialCreationOptionsJSON) có các trường được mã hoá Base64URL. - Trên ứng dụng, các trình duyệt hiện đại hỗ trợ
PublicKeyCredential.parseCreationOptionsFromJSON()để chuyển đổi trực tiếp các tuỳ chọn JSON thànhPublicKeyCredentialCreationOptions. Khi sử dụng thư viện phía máy khách như@simplewebauthn/browser, các hàm nhưstartRegistration({ optionsJSON: options })sẽ tự động xử lý lượt chuyển đổi này.
Mã ví dụ: tạo các lựa chọn tạo thông tin xác thực
Chúng tôi sử dụng thư viện SimpleWebAuthn trong các ví dụ của mình. Tại đây, chúng ta chuyển giao việc tạo các tuỳ chọn thông tin xác thực khoá công khai cho hàm generateRegistrationOptions.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: cred.id,
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = await generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: isoUint8Array.fromUTF8String(user.id),
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
residentKey: 'required',
userVerification: 'preferred',
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Lưu trữ khoá công khai
navigator.credentials.create trả về một đối tượng PublicKeyCredential.Khi navigator.credentials.create phân giải thành công trên ứng dụng, tức là khoá truy cập đã được tạo thành công. Hệ thống sẽ trả về một đối tượng PublicKeyCredential.
Đối tượng PublicKeyCredential chứa một đối tượng AuthenticatorAttestationResponse, đại diện cho phản hồi của nhà cung cấp khoá truy cập đối với hướng dẫn của ứng dụng khách về việc tạo khoá truy cập. Phản hồi này chứa thông tin về thông tin xác thực mới mà bạn cần với tư cách là một RP để xác thực người dùng sau này. Tìm hiểu thêm về AuthenticatorAttestationResponse trong Phụ lục: AuthenticatorAttestationResponse.
Gửi đối tượng PublicKeyCredential đến máy chủ. Sau khi nhận được mã, hãy xác minh mã đó.
Chuyển bước xác minh này sang thư viện phía máy chủ FIDO. Thông thường, thư viện sẽ cung cấp một hàm hiệu dụng cho mục đích này. Ví dụ: SimpleWebAuthn cung cấp verifyRegistrationResponse. Tìm hiểu những gì đang diễn ra trong Phụ lục: xác minh phản hồi đăng ký.
Sau khi xác minh thành công, hãy lưu trữ thông tin đăng nhập trong cơ sở dữ liệu để sau này người dùng có thể xác thực bằng khoá truy cập liên kết với thông tin đăng nhập đó.
Sử dụng một bảng riêng cho thông tin xác thực khoá công khai liên kết với khoá truy cập. Người dùng chỉ có thể có một mật khẩu nhưng có thể có nhiều khoá truy cập, chẳng hạn như một khoá truy cập được đồng bộ hoá qua Chuỗi khoá iCloud của Apple và một khoá truy cập được đồng bộ hoá qua Trình quản lý mật khẩu của Google.
Dưới đây là giản đồ mẫu mà bạn có thể dùng để lưu trữ thông tin xác thực:

- Bảng Người dùng:
user_id: Mã nhận dạng người dùng chính. Mã nhận dạng ngẫu nhiên, duy nhất và vĩnh viễn cho người dùng. Sử dụng giá trị này làm khoá chính cho bảng Người dùng.username. Tên người dùng do người dùng xác định, có thể chỉnh sửa được.passkey_user_id: Mã nhận dạng người dùng không chứa thông tin nhận dạng cá nhân (PII) dành riêng cho khoá truy cập, được biểu thị bằnguser.idtrong các lựa chọn đăng ký. Khi người dùng tìm cách xác thực sau đó, trình xác thực sẽ cung cấppasskey_user_idnày trong phản hồi xác thực tronguserHandle. Bạn không nên đặtpasskey_user_idlàm khoá chính. Khoá chính có xu hướng trở thành PII thực tế trong các hệ thống vì chúng được sử dụng rộng rãi.
- Bảng thông tin xác thực khoá công khai:
id: Mã thông tin xác thực. Sử dụng giá trị này làm khoá chính cho bảng Thông tin xác thực khoá công khai.public_key: Khoá công khai của thông tin xác thực.passkey_user_id: Sử dụng trường này làm khoá ngoại để thiết lập mối liên kết với bảng Người dùng.backed_up: Khoá truy cập được sao lưu nếu được nhà cung cấp khoá truy cập đồng bộ hoá. Việc lưu trữ trạng thái sao lưu sẽ hữu ích nếu bạn muốn cân nhắc việc bỏ mật khẩu trong tương lai cho những người dùng có khoá truy cậpbacked_up. Bạn có thể kiểm tra xem khoá truy cập có được sao lưu hay không bằng cách kiểm tra cờ BE trongauthenticatorDatahoặc bằng cách sử dụng tính năng thư viện phía máy chủ FIDO thường có sẵn để giúp bạn dễ dàng truy cập vào thông tin này. Việc lưu trữ trạng thái đủ điều kiện sao lưu có thể giúp giải quyết các thắc mắc của người dùng.name: Tên hiển thị cho thông tin xác thực (không bắt buộc) để người dùng có thể đặt tên tuỳ chỉnh cho thông tin xác thực.transports: Một mảng phương thức truyền tải. Việc lưu trữ phương thức truyền tải sẽ hữu ích cho trải nghiệm xác thực của người dùng. Khi có phương thức truyền tải, trình duyệt có thể hoạt động tương ứng và hiển thị giao diện người dùng phù hợp với phương thức truyền tải mà nhà cung cấp khoá truy cập sử dụng để giao tiếp với ứng dụng, đặc biệt là đối với các trường hợp sử dụng xác thực lại khiallowCredentialskhông trống.
Bạn nên lưu trữ các thông tin khác để cải thiện trải nghiệm người dùng, bao gồm cả những mục như trình cung cấp khoá truy cập, thời gian tạo thông tin xác thực và thời gian sử dụng gần đây nhất. Đọc thêm trong bài viết Thiết kế giao diện người dùng cho khoá truy cập.
Mã ví dụ: lưu trữ thông tin xác thực
Chúng tôi sử dụng thư viện SimpleWebAuthn trong các ví dụ của mình.
Tại đây, chúng ta chuyển việc xác minh phản hồi đăng ký sang hàm verifyRegistrationResponse.
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const {
aaguid,
credential,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '00000000-0000-0000-0000-000000000000' ?
req.useragent?.platform : aaguids[aaguid]?.name;
const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: credential.id,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
transports: credential.transports || response.response.transports,
backed_up: credentialBackedUp,
registered_at: new Date().getTime()
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Phụ lục: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse chứa hai đối tượng quan trọng:
response.clientDataJSONlà phiên bản JSON của dữ liệu ứng dụng, trên web là dữ liệu mà trình duyệt nhìn thấy. Đối tượng này chứa nguồn gốc RP, thử thách vàandroidPackageNamenếu ứng dụng là một ứng dụng Android. Là một RP, việc đọcclientDataJSONsẽ giúp bạn truy cập vào thông tin mà trình duyệt thấy tại thời điểm yêu cầucreate.response.attestationObjectchứa hai thông tin:attestationStatementkhông liên quan trừ phi bạn sử dụng tính năng chứng thực.authenticatorDatalà dữ liệu mà nhà cung cấp khoá truy cập nhìn thấy. Là một bên Relying Party, việc đọcauthenticatorDatacho phép bạn truy cập vào dữ liệu mà nhà cung cấp khoá truy cập nhìn thấy và trả về tại thời điểm yêu cầucreate.
authenticatorDatachứa thông tin cần thiết về thông tin xác thực khoá công khai liên kết với khoá truy cập mới tạo:
- Bản thân thông tin xác thực khoá công khai và mã thông tin xác thực duy nhất cho thông tin xác thực đó.
- Mã RP liên kết với thông tin xác thực.
- Cờ mô tả trạng thái của người dùng khi khoá truy cập được tạo: liệu người dùng có thực sự hiện diện hay không và liệu người dùng có được xác minh thành công hay không (xem phần tìm hiểu sâu về userVerification).
- AAGUID là giá trị nhận dạng của nhà cung cấp khoá truy cập, chẳng hạn như Trình quản lý mật khẩu của Google. Dựa trên AAGUID, bạn có thể xác định trình cung cấp khoá truy cập và hiển thị tên trong trang quản lý khoá truy cập. (xem phần Xác định nhà cung cấp khoá truy cập bằng AAGUID)
Mặc dù authenticatorData được lồng trong attestationObject, nhưng thông tin có trong đó là cần thiết cho việc triển khai khoá truy cập của bạn, cho dù bạn có sử dụng chứng thực hay không. authenticatorData được mã hoá và chứa các trường được mã hoá ở định dạng nhị phân. Thư viện phía máy chủ của bạn thường sẽ xử lý việc phân tích cú pháp và giải mã. Nếu bạn không sử dụng thư viện phía máy chủ, hãy cân nhắc việc tận dụng getAuthenticatorData() phía máy khách để tiết kiệm một số công việc phân tích cú pháp và giải mã phía máy chủ.
Phụ lục: xác minh phản hồi đăng ký
Về cơ bản, việc xác minh phản hồi đăng ký bao gồm các bước kiểm tra sau:
- Đảm bảo rằng mã RP khớp với trang web của bạn.
- Đảm bảo rằng nguồn gốc của yêu cầu là nguồn gốc dự kiến cho trang web của bạn (URL trang web chính, ứng dụng Android).
- Nếu bạn yêu cầu xác minh người dùng, hãy đảm bảo rằng cờ xác minh người dùng
authenticatorData.uvlàtrue. - Cờ hiện diện của người dùng
authenticatorData.upthường được dự kiến làtrue, nhưng nếu thông tin xác thực được tạo có điều kiện, thì cờ này dự kiến làfalse. - Kiểm tra để đảm bảo ứng dụng có thể cung cấp thử thách mà bạn đã đưa ra. Nếu bạn không sử dụng quy trình chứng thực, thì quy trình kiểm tra này không quan trọng. Tuy nhiên, bạn nên triển khai quy trình kiểm tra này vì đây là phương pháp hay nhất: quy trình này đảm bảo mã của bạn đã sẵn sàng nếu bạn quyết định sử dụng quy trình chứng thực trong tương lai.
- Đảm bảo rằng mã thông tin xác thực chưa được đăng ký cho bất kỳ người dùng nào.
- Xác minh rằng thuật toán mà nhà cung cấp khoá truy cập sử dụng để tạo thông tin xác thực là thuật toán mà bạn đã liệt kê (trong mỗi trường
algcủapublicKeyCredentialCreationOptions.pubKeyCredParams, thường được xác định trong thư viện phía máy chủ và bạn không nhìn thấy). Điều này đảm bảo rằng người dùng chỉ có thể đăng ký bằng các thuật toán mà bạn đã chọn cho phép.
Để tìm hiểu thêm, hãy xem mã nguồn của SimpleWebAuthn cho verifyRegistrationResponse hoặc tìm hiểu danh sách đầy đủ các quy trình xác minh trong thông số kỹ thuật.