如何使用 webauthn-json 进行编码

how to use webauthn-json to encode

我正在尝试在 rails 应用程序中实施 webauthn,我正在尝试遵循此 github 存储库:

https://github.com/cedarcode/webauthn-ruby

我的服务器挑战运行良好,它被发送到浏览器:

options = WebAuthn::Credential.options_for_create(
        user: { id: current_user.webauthn_id, name: current_user.email },
        exclude: current_user.credentials.map { |c| c.webauthn_id }
    )

    # Store the newly generated challenge somewhere so you can have it
    # for the verification phase.
    session[:creation_challenge] = options.challenge
    binding.pry
    render json: options

现在,在浏览器端(javascript),我正在尝试做:

navigator.credentials.create({"publickey": window.webauthn_options})

(注意,我将来自控制器的选项存储在 window 变量中),但是当我这样做时,我收到浏览器控制台错误:

TypeError: CredentialsContainer.create: Missing required 'challenge' member of PublicKeyCredentialCreationOptions.

在调试器中,我发现挑战确实存在,但我猜这与某种编码问题有关。关于 base64url 编码和使用此 repo 编码/解码的参考非常模糊:

https://github.com/github/webauthn-json/

但我不知道如何使用它,它似乎是一个 nodejs 包(?)我期待一个 javascript 文件(我不是节点程序员)。所以我想我的问题是:

1) 如何使用节点 js 包(不确定这个 repo 是不是那个,只是猜测)来制作一个 js 文件,我可以将其部署到 rails 应用程序中的服务器?

2) 这个错误是否意味着即使挑战存在,它也没有正确编码?

感谢您的帮助, 凯文

更新

感谢@mackie 的精彩回答,fido 网站有很多有用的东西,他们的 js 帮了大忙,在这里补充说以防将来的 webauthn 开发人员需要大量的时间节省:

https://www.passwordless.dev/js/mfa.register.js

管理服务器和客户端之间的转换并确保一切都是正确的类型有点麻烦,但下面的示例对我有用。我使用了 https://github.com/abergs/fido2-net-lib 提供的示例,发现它非常有用。

下面是我的第二个因素凭据创建选项服务器端点返回的示例 JSON 数据结构(WebAuthn 指定为 ArrayBuffers 的属性具有 base64url 编码值,我还缩短了 pubKeyCredParams 数组):

{
    "rp": {
        "id": "localhost",
        "name": "IDS4"
    },
    "user": {
        "name": "joe.bloggs@acme.com",
        "id": "YTNmZTAxYWUtODlhYS00NDEzLTgxYzQtZWJmZjk0MmI5MTVj",
        "displayName": "Your ACME account"
    },
    "challenge": "P8_m1vd5tcMDD9e0SeST4w",
    "pubKeyCredParams": [
        {
            "type": "public-key",
            "alg": -7
        }
    ],
    "timeout": 60000,
    "attestation": "indirect",
    "authenticatorSelection": {
        "authenticatorAttachment": "cross-platform",
        "requireResidentKey": false,
        "userVerification": "discouraged"
    },
    "excludeCredentials": [
        {
            "type": "public-key",
            "id": "A_IySAe38xFIoTUbAFyAUIrgawhcPOD_xbBDf_UqkvJc_GR37-jRXccYE04A5CmhA3kG8WTGPZP63MiQQ2ykDQ"
        }
    ],
    "extensions": {
        "exts": true,
        "uvi": true,
        "loc": true,
        "uvm": true,
        "biometricPerfBounds": {
            "FAR": 3.4028235E+38,
            "FRR": 3.4028235E+38
        }
    }
}

需要强制转换为 ArrayBuffer 的值是:

  • challenge
  • user.id
  • excludeCredentials[n].id

辅助函数 - WebAuthnHelpers.js:

class WebAuthnHelpers {
    static coerceToArrayBuffer(input) {
        if (typeof input === "string") {
            // base64url to base64
            input = input.replace(/-/g, "+").replace(/_/g, "/");

            // base64 to Uint8Array
            var str = window.atob(input);
            var bytes = new Uint8Array(str.length);
            for (var i = 0; i < str.length; i++) {
                bytes[i] = str.charCodeAt(i);
            }
            input = bytes;
        }

        // Array to Uint8Array
        if (Array.isArray(input)) {
            input = new Uint8Array(input);
        }

        // Uint8Array to ArrayBuffer
        if (input instanceof Uint8Array) {
            input = input.buffer;
        }

        // error if none of the above worked
        if (!(input instanceof ArrayBuffer)) {
            throw new TypeError("could not coerce '" + name + "' to ArrayBuffer");
        }

        return input;
    }

    static coerceToBase64Url(input) {
        // Array or ArrayBuffer to Uint8Array
        if (Array.isArray(input)) {
            input = Uint8Array.from(input);
        }

        if (input instanceof ArrayBuffer) {
            input = new Uint8Array(input);
        }

        // Uint8Array to base64
        if (input instanceof Uint8Array) {
            var str = "";
            var len = input.byteLength;

            for (var i = 0; i < len; i++) {
                str += String.fromCharCode(input[i]);
            }
            input = window.btoa(str);
        }

        if (typeof input !== "string") {
            throw new Error("could not coerce to string");
        }

        // base64 to base64url
        // NOTE: "=" at the end of challenge is optional, strip it off here
        input = input.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");

        return input;
    }
}

例如

credentialCreateOptions.challenge = WebAuthnHelpers.coerceToArrayBuffer(credentialCreateOptions.challenge);

credentialCreateOptions.user.id = WebAuthnHelpers.coerceToArrayBuffer(credentialCreateOptions.user.id);

credentialCreateOptions.excludeCredentials = credentialCreateOptions.excludeCredentials.map((c) =>
{
    c.id = WebAuthnHelpers.coerceToArrayBuffer(c.id);
    return c;
});

if (credentialCreateOptions.authenticatorSelection.authenticatorAttachment === null) credentialCreateOptions.authenticatorSelection.authenticatorAttachment = undefined;

完成后,我可以将 credentialCreateOptions 直接传递给 navigator.credentials.create({publicKey: ... })