FIDO2跨设备无缝登录(Passkeys)配置与故障排查问询
编辑2
为进一步说明@Tim的解释,我对credential.create的Authenticator Selection Options做了如下修改:
authenticatorSelection: { //默认值为false,我显式指定了该参数 requireResidentKey: false, //默认值为"preferred",若不指定Chrome会给出警告 //不确定是否有实际功能影响 userVerification: "discouraged", //此项为功能正常运行所必需 authenticatorAttachment: "cross-platform" },
除首次仍提示USB加密狗外,功能运行完美!
它甚至会在你忘记开启蓝牙时自动打开。太赞了!(它是如何识别我的手机的?是因为我在电脑和手机上登录了同一个Chrome账号?还是通过注册表?)
无论如何,这个功能简直太棒了!我一直努力推动澳大利亚珀斯的网站采用FIDO2,而这个功能无疑是关键推手。
编辑2结束
编辑开始
如果你只是想测试使用手机进行的新FIDO跨设备认证流程,
没错,这正是我想要实现的目标。我现在已恢复到以下配置:
- 安卓手机开启蓝牙
- 蓝牙设置为可见状态
- 电脑/Chrome和手机登录同一Google账号
- Windows账户已设置Windows Hello PIN
但以下代码仅给出输入PIN的选项,没有“添加新安卓手机”的提示
“你在这里混淆了几个不同的概念。安全密钥、本地平台认证器(Windows Hello)和你的手机各自拥有独立的凭证。”
我确信你是对的。我只是尝试了所有我知道的方法来让它运行:Windows Phone Link、微软账户相关设置等
“不要要求驻留凭证(目前尚不支持),也不要设置附件偏好。”
我不太明白这是什么意思。如果指的是USB密钥,那没问题,我不想使用它,但系统仍会提示我使用它。(见下文)
“当Chrome中出现提示时,添加你的手机进行关联,在手机上扫描二维码,然后执行用户验证(UV)操作。”
好的,你使用的是什么二维码阅读器?
我的问题是Chrome没有提示我添加手机:-( 是不是我缺少了某个配置或API参数?
请帮忙解答。
编辑结束
我了解“今年晚些时候向开发者开放”的说明,但作为FIDO技术爱好者,我对Passkeys预览内容(9分钟后部分)中展示的功能感到非常兴奋,而Chrome/Samsung/Windows当前版本中的潜在支持更让我跃跃欲试!
内容摘要
- 我已将三星安卓手机与Windows电脑配对
- 我已启用Yubikey USB设备(未安装Yubikey软件)
- 调用
navigator.credentials.create时,Windows提示我输入PIN - 调用
navigator.credentials.get时,Windows提示无法识别我的YubiKey
- 取消后,系统提示选择其他设备

- 如果我选择我的SM_*手机,手机会提示我输入指纹,三星安卓系统验证通过!
- 但Chrome验证失败

因此,尽管我承认这是一项“新兴”技术,但仍有以下疑问:
- 为什么创建凭证时提示输入PIN而非指纹?
- 我尝试过设置各种Google账户、Windows Live和Windows账户选项,哪些选项会影响该功能?
- 我的手机已通过蓝牙与Windows配对,这对Chrome来说还不够吗?
- 如果我选择“添加新安卓手机”,会生成一个恐龙样式的二维码。我的手机上只有三星浏览器可以扫描二维码,扫描后返回FIDO:/484543913687778941263973123987003762051850670080716404329165 . . . 但Chrome无法识别,该如何处理?
- 以下是安卓/三星提示我进行生物识别验证的截图:

源代码
const utf8Decoder = new TextDecoder('utf-8'); async function verifyCredential() { var keyResult = await getKey(); var serverChallenge = JSON.parse(keyResult); var credentialId = localStorage.getItem("credentialId"); if (!credentialId) { throw new Error("You must create a Credential first"); } var allowCredentials = [{ type: "public-key", id: Uint8Array.from(atob(credentialId), x => x.charCodeAt(0)).buffer }] var getAssertionOptions = { timeout: 30000, challenge: Uint8Array.from(serverChallenge.Token, c => c.charCodeAt(0)).buffer, allowCredentials: allowCredentials, userVerification: "required" }; return navigator.credentials.get({ publicKey: getAssertionOptions }).then(rawAssertion => { var assertion = { id: base64encode(rawAssertion.rawId), clientDataJSON: utf8Decoder.decode(rawAssertion.response.clientDataJSON), userHandle: base64encode(rawAssertion.response.userHandle), signature: base64encode(rawAssertion.response.signature), authenticatorData: base64encode(rawAssertion.response.authenticatorData) }; // Check id = allowcredentials.id console.log("=== Assertion response ==="); console.log(assertion); verifyAssertion(assertion).then( result => { var res = JSON.parse(result); console.log(res.success); if (res.success) { } }); return; }).catch( (err) => { if (err.name == "NotAllowedError") { console.log("here " + err.name); } else { console.log("other " + err.name); } return Promise.resolve(false); }); } async function createCredential() { var keyResult = await getKey(); var serverChallenge = JSON.parse(keyResult); var createCredentialOptions = { rp: { name: "WebAuthn Sample App", icon: "" }, user: { id: Uint8Array.from("some.user.guid", c => c.charCodeAt(0)), name: "maherrj@gmail.com", displayName: "Richard Maher", icon: "" }, pubKeyCredParams: [ { //External authenticators support the ES256 algorithm type: "public-key", alg: -7 }, { //Windows Hello supports the RS256 algorithm type: "public-key", alg: -257 } ], authenticatorSelection: { //Select authenticators that support username-less flows //requireResidentKey: true, //Select authenticators that have a second factor (e.g. PIN, Bio) "preferred" "discouraged" userVerification: "required", //Selects between bound or detachable authenticators authenticatorAttachment: "platform" // Optional }, //Since Edge shows UI, it is better to select larger timeout values timeout: 30000, //an opaque challenge that the authenticator signs over challenge: Uint8Array.from(serverChallenge.Token, c => c.charCodeAt(0)).buffer, //prevent re-registration by specifying existing credentials here excludeCredentials: [], //specifies whether you need an attestation statement attestation: "none" }; const authAbort = new AbortController(); const abortSignal = authAbort.signal; abortSignal.addEventListener("abort", (e) => { console.log("It has been aborted"); }); return navigator.credentials.create({ publicKey: createCredentialOptions, signal: abortSignal }).then(rawAttestation => { var attestation = { id: base64encode(rawAttestation.rawId), clientDataJSON: utf8Decoder.decode(rawAttestation.response.clientDataJSON), attestationObject: base64encode(rawAttestation.response.attestationObject) }; console.log("=== Attestation response ==="); console.log(attestation); verifyCredentials(attestation).then( result => { var res = JSON.parse(result); console.log(res.success); if (res.success) { localStorage.setItem("credentialId", res.id); } }); return; }).catch( (err) => { if (err.name == "NotAllowedError") { console.log("here " + err.name); } else { console.log("other " + err.name); } return Promise.resolve(false); }); } async function verifyCredentials(attestation) { let params = JSON.stringify(attestation); let resp = await fetch("api/fido/verifycredentials", { method: "POST", headers: { "Content-type": "application/json", "Accept": "application/json" }, body: params }); var myStat; if (resp.ok) { myStat = await resp.json(); console.log("Stat vc = " + myStat) } else { console.log("boom"); } console.log("done "); return myStat; } async function verifyAssertion(assertion) { let params = JSON.stringify(assertion); let resp = await fetch("api/fido/verifyassertion", { method: "POST", headers: { "Content-type": "application/json", "Accept": "application/json" }, body: params }); var myStat; if (resp.ok) { myStat = await resp.json(); console.log("Stat va = " + myStat) } else { console.log("boom"); } console.log("done "); return myStat; } async function getKey() { let resp = await fetch("api/fido/getkey", { method: "GET", headers: { "Content-type": "application/json", "Accept": "application/json" } }); var mykey; if (resp.ok) { mykey = await resp.json(); console.log("key = " + mykey) } else { throw new Error("boom"); } console.log("done key"); return mykey; } function base64encode(arrayBuffer) { if (!arrayBuffer || arrayBuffer.length == 0) return undefined; return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))); }
内容的提问来源于stack exchange,提问作者McMurphy

