WebAuthn服务端(Go)与客户端(JS)挑战验证失败求助
WebAuthn注册验证失败:JS编解码函数修复方案
问题核心
服务端基于Go + go-webauthn/webauthn库,纯JS客户端实现WebAuthn设备注册流程,点击按钮触发注册后,触摸Yubikey传感器完成操作,但服务端CreateCredential()抛出验证失败错误。怀疑问题出在JS的buffer_encode和buffer_decode函数,且不理解buffer_encode中replace()语句的作用。
问题分析
1. buffer_encode中replace()的作用
btoa()生成的是标准Base64编码,但WebAuthn规范要求使用Base64 URL安全编码(避免URL转义、兼容JSON传输),三个replace()的作用就是做格式转换:
- 把
+替换为- - 把
/替换为_ - 去掉末尾的
=填充字符
移除这些替换后,服务端无法解析非标准Base64格式的数据,导致验证失败。
2. 当前buffer_decode的错误
服务端返回的challenge和user.id是Base64 URL编码的字符串,而你当前的buffer_decode直接将字符串的每个字符转成Unicode编码值,完全不符合Base64解码逻辑,导致传给浏览器的challenge和user.id数据错误,这是验证失败的核心原因。
正确的编解码函数
// 将ArrayBuffer转为Base64 URL安全编码(符合WebAuthn规范) const buffer_encode = (arrayBuffer) => { return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); }; // 将Base64 URL编码字符串转为ArrayBuffer const buffer_decode = (base64UrlStr) => { // 先将Base64 URL转成标准Base64格式 let base64Str = base64UrlStr.replace(/-/g, "+").replace(/_/g, "/"); // 补充缺失的填充字符= while (base64Str.length % 4 !== 0) { base64Str += "="; } // 解码成二进制字符串,再转成Uint8Array const binaryStr = atob(base64Str); return Uint8Array.from(binaryStr, c => c.charCodeAt(0)); };
修正后的完整JS代码
const register_device = async (event) => { event.preventDefault(); // 修正后的编解码函数 const buffer_encode = (arrayBuffer) => { return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); }; const buffer_decode = (base64UrlStr) => { let base64Str = base64UrlStr.replace(/-/g, "+").replace(/_/g, "/"); while (base64Str.length % 4 !== 0) { base64Str += "="; } const binaryStr = atob(base64Str); return Uint8Array.from(binaryStr, c => c.charCodeAt(0)); }; if (!window.PublicKeyCredential) { alert("Error: 该浏览器不支持WebAuthn"); return; } try { // 发起注册初始化请求 const beginResponse = await fetch("/2fa/v1/webauthn/register/begin"); const credentialCreationOptions = await beginResponse.json(); // 解码服务端返回的challenge和user.id credentialCreationOptions.publicKey.challenge = buffer_decode( credentialCreationOptions.publicKey.challenge ); credentialCreationOptions.publicKey.user.id = buffer_decode( credentialCreationOptions.publicKey.user.id ); // 调用WebAuthn API创建凭证 const credential = await navigator.credentials.create({ publicKey: credentialCreationOptions.publicKey }); // 编码凭证数据,发送给服务端完成注册 const finishResponse = await fetch("/2fa/v1/webauthn/register/finish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: credential.id, rawId: buffer_encode(credential.rawId), type: credential.type, response: { attestationObject: buffer_encode(credential.response.attestationObject), clientDataJSON: buffer_encode(credential.response.clientDataJSON), } }) }); const result = await finishResponse.json(); console.log(`注册结果: 状态码=${finishResponse.status} 消息=${JSON.stringify(result)}`); } catch (error) { console.error("注册过程中发生错误:", error); } }; device.addEventListener("click", register_device);
额外验证建议
- 查看服务端具体错误信息:在Go代码中打印
CreateCredential()抛出的错误详情,比如是challenge不匹配、数据格式错误还是签名验证失败,这能更精准定位问题。 - 检查sessionData有效性:确保注册初始化时生成的
sessionData被正确存储(比如存在session或Redis中),且在finish接口中能正确获取到,因为CreateCredential()依赖该数据做验证。
内容的提问来源于stack exchange,提问作者Christian Rößner
相关产品推荐
相关产品推荐

