本地环境WebAuthn调用navigator.credentials.get()失败求助
问题:FIDO2登录提示无可用注册密钥
问题背景
我正在开发一个测试Fido2-server工作流程的示例应用,完成注册(Mac Touch ID或YubiKey 5系列)后,Chrome提示该网站无已注册的密钥。
已执行步骤
- 携带用户ID发起注册请求(配置Fido2Lib)并返回challenge
- 接收服务器返回的注册选项,执行
navigator.credentials.create - 将凭证发送至服务器,为用户保存公钥
- 携带用户ID发起登录请求,返回包含
allowCredentials的FIDO2 assertionOptions - 尝试调用
navigator.credentials.get,弹出提示「该网站无可用密钥」
不同认证器的错误表现
- 使用Touch ID(平台型)时,提示「localhost网站无可用密钥」
- 使用YubiKey(跨平台型)时,提示「该安全密钥未注册至本网站」
怀疑是challenge或allowCredential的处理有误导致navigator.credentials.get调用失败,以下是关键代码片段:
注册请求(registerRequest)
const fido2 = new Fido2Lib({ timeout: 60000, rpId: "localhost", rpName: "localhost:3000", rpIcon: "https://example.com/icon.png", challengeSize: 32, attestation: "direct", authenticatorAttachment: "cross-platform", authenticatorRequireResidentKey: false, authenticatorUserVerification: "preferred" });
const user = users[userId] || { id: userId, name: username, displayName: username, }; users[userId] = user; const options = await fido2.attestationOptions(); options.user = user; options.challenge = base64url.encode(options.challenge); if (options.user && options.user.id) { options.user.id = base64url.encode(Buffer.from(options.user.id, 'utf-8')); }
注册响应(registerResponse)
req.body.rawId = new Uint8Array(base64url.toBuffer(req.body.id)).buffer; const attestationResult = await fido2.attestationResult(req.body, { challenge: user.challenge, origin: "http://localhost:3000", factor: "either" }); if (attestationResult.audit.complete) { const d = attestationResult.authnrData; user.authenticator = JSON.stringify({ publickey : d.get('credentialPublicKeyPem'), counter : d.get('counter'), fmt: d.get('fmt'), credId : base64url.encode(d.get('credId')), }); users[userId] = user;
登录响应(loginResponse)
const user = users[userId]; const authenticator = JSON.parse(user.authenticator); const assertionOptions = await fido2.assertionOptions(); assertionOptions.challenge = base64url.encode(assertionOptions.challenge); let allowCredentials = []; allowCredentials.push({ type: 'public-key', id: authenticator.credId, transports: ['usb', 'nfc', 'ble'] // transports: ['internal'] }); assertionOptions.allowCredentials = allowCredentials;
客户端注册代码(client.js for register)
const options = response.data; options.challenge = Uint8Array.from(atob(options.challenge), (c) => c.charCodeAt(0)); options.user.id = Uint8Array.from(atob(options.user.id), (c) => c.charCodeAt(0)); const credential = await navigator.credentials.create({ publicKey: options }) as PublicKeyCredential; const res = publicKeyCredentialToJSON(credential.response); // publicKeyCredentialToJSON is kind of base64encode if (credential) { const attestationResponse = { id: credential.id, rawId: Array.from(new Uint8Array(credential.rawId)), type: credential.type, response: res };
客户端登录代码(client.js for login)
const options = response.data; options.challenge = Uint8Array.from(base64url.decode(options.challenge)).buffer; options.allowCredentials = options.allowCredentials.map((c: any) => { c.id = Uint8Array.from(base64url.decode(c.id)).buffer; return c; }); const assertion = await navigator.credentials.get({ publicKey: options }) as PublicKeyCredential;
Mac上已成功保存localhost的passkey,若有缺失信息请告知,或帮我指出代码中的错误。
代码错误定位与修正建议
1. 注册时编码/解码不匹配(base64 vs base64url)
服务器端用base64url.encode处理challenge和用户ID,但客户端注册代码用atob(仅支持标准base64)解码,导致数据解析错误,后续登录验证不通过。
- 修正客户端注册代码:
// 原代码 options.challenge = Uint8Array.from(atob(options.challenge), (c) => c.charCodeAt(0)); options.user.id = Uint8Array.from(atob(options.user.id), (c) => c.charCodeAt(0)); // 改为 options.challenge = Uint8Array.from(base64url.decode(options.challenge)); options.user.id = Uint8Array.from(base64url.decode(options.user.id));
2. AllowCredentials的Transports配置错误
登录时统一设置transports: ['usb', 'nfc', 'ble'],但Touch ID属于平台型认证器,对应transport为internal,导致浏览器无法匹配凭证。
- 修正步骤:
- 注册时保存认证器的实际transport信息:
// 注册响应中修改保存逻辑 user.authenticator = JSON.stringify({ publickey : d.get('credentialPublicKeyPem'), counter : d.get('counter'), fmt: d.get('fmt'), credId : base64url.encode(d.get('credId')), transports: req.body.response.transports // 从注册请求中获取真实transport });- 登录时使用保存的transport:
// 登录响应中修改allowCredentials配置 allowCredentials.push({ type: 'public-key', id: authenticator.credId, transports: authenticator.transports || ['usb', 'nfc', 'ble', 'internal'] });
3. 客户端登录Challenge处理冗余
客户端登录代码中对Challenge的转换冗余,base64url.decode返回的Buffer可直接转换为ArrayBuffer:
- 修正:
// 原代码 options.challenge = Uint8Array.from(base64url.decode(options.challenge)).buffer; // 改为 options.challenge = base64url.decode(options.challenge).buffer;
内容的提问来源于stack exchange,提问作者gattoman
相关产品推荐
相关产品推荐

