0

我正在尝试在 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})

(注意,我将控制器中的选项存储在窗口变量中),但是当我这样做时,我得到一个浏览器控制台错误:

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

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

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

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

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

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

感谢您的帮助,凯文

更新

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

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

4

1 回答 1

2

管理服务器和客户端之间的转换并确保一切都是正确的类型有点痛苦,但下面的示例对我有用。我使用了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: ... })

于 2020-05-11T08:12:12.490 回答