You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 无效USB
  • 取消后,系统提示选择其他设备 设备选择
  • 如果我选择我的SM_*手机,手机会提示我输入指纹,三星安卓系统验证通过!
  • 但Chrome验证失败 验证失败

因此,尽管我承认这是一项“新兴”技术,但仍有以下疑问:

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

源代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 19:01:25