0

I have been trying to figure out how to do 2fa with webauthn and I have the registration part working. The details are really poorly documented, especially all of the encoding payloads in javascript. I am able to register a device to a user, but I am not able to authenticate with that device. For reference, I'm using these resources:

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

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

And specifically, for authentication, I'm trying to mimic this js functionality:

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

In my user model, I have a webauthn_id, and several u2f devices, each of which has a public_key and a webauthn_id.

In my Rails app, I do:

options = WebAuthn::Credential.options_for_get(allow: :webauthn_id)
session[:webauthn_options] = options

In my javascript, I try to mimic the js file above and I do (this is embedded ruby):

options = <%= raw @options.as_json.to_json %>
options.challenge = WebAuthnHelpers.coerceToArrayBuffer(options.challenge);
options.allowCredentials = options.allowCredentials.map((c) => {
    c.id = WebAuthnHelpers.coerceToArrayBuffer(c.id);
    return c;
});

navigator.credentials.get({ "publicKey": options }).then(function (credentialInfoAssertion) 
{
            // send assertion response back to the server
            // to proceed with the control of the credential
alert('here');
}).catch(function (err) 
{
    debugger
    console.error(err);   /*  THIS IS WHERE THE ERROR IS THROWN */
});

The problem is, I cannot get past navigator.credentials.get, I get this error in the javascript console:

TypeError: CredentialsContainer.get: Element of 'allowCredentials' member of PublicKeyCredentialRequestOptions can't be converted to a dictionary

options at the time navigator.credentials.get is called looks like this:

enter image description here

I've tried every which way to convert my db-stored user and device variables into javascript properly encoded and parsed variables but cannot seem to get it to work. Anything obvious about what I'm doing wrong?

Thanks for any help, Kevin

UPDATE -

Adding options json generated by the server:

"{\"challenge\":\"SSDYi4I7kRWt5wc5KjuAvgJ3dsQhjy7IPOJ0hvR5tMg\",\"timeout\":120000,\"allowCredentials\":[{\"type\":\"public-key\",\"id\":\"OUckfxGNLGGASUfGiX-1_8FzehlXh3fKvJ98tm59mVukJkKb_CGk1avnorL4sQQASVO9aGqmgn01jf629Jt0Z0SmBpDKd9sL1T5Z9loDrkLTTCIzrIRqhwPC6yrkfBFi\"},{\"type\":\"public-key\",\"id\":\"Fj5T-WPmEMTz139mY-Vo0DTfsNmjwy_mUx6jn5rUEPx-LsY51mxNYidprJ39_cHeAOieg-W12X47iJm42K0Tsixj4_Fl6KjdgYoxQtEYsNF-LPhwtoKwYsy1hZgVojp3\"}]}"
user1130176
  • 1,772
  • 1
  • 23
  • 33

1 Answers1

0

This is an example of the serialised JSON data returned by our implementation:

{
    "challenge": "MQ1S8MBSU0M2kiJqJD8wnQ",
    "timeout": 60000,
    "rpId": "identity.acme.com",
    "allowCredentials": [
        {
            "type": "public-key",
            "id": "k5Ti8dLdko1GANsBT-_NZ5L_-8j_8TnoNOYe8mUcs4o",
            "transports": [
                "internal"
            ]
        },
        {
            "type": "public-key",
            "id": "LAqkKEO99XPCQ7fsUa3stz7K76A_mE5dQwX4S3QS6jdbI9ttSn9Hu37BA31JUGXqgyhTtskL5obe6uZxitbIfA",
            "transports": [
                "usb"
            ]
        },
        {
            "type": "public-key",
            "id": "nbN3S08Wv2GElRsW9AmK70J1INEpwIywQcOl6rp_DWLm4mcQiH96TmAXSrZRHciZBENVB9rJdE94HPHbeVjtZg",
            "transports": [
                "usb"
            ]
        }
    ],
    "userVerification": "discouraged",
    "extensions": {
        "txAuthSimple": "Sign in to your ACME account",
        "exts": true,
        "uvi": true,
        "loc": true,
        "uvm": true
    }
}

This is parsed to an object and the code used to coerce those base64url encoded values is:

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

credentialRequestOptions.allowCredentials = credentialRequestOptions.allowCredentials.map((c) => {
    c.id = WebAuthnHelpers.coerceToArrayBuffer(c.id);
    return c;
});

Hope that helps. The JSON data is retreived via a fetch() call and the byte[] fields are encoded as base64url on the serverside.

mackie
  • 4,996
  • 1
  • 17
  • 17
  • thanks @mackie, the only difference between your payload and mine is i don't have an rpId, userVerificaiton, or extensions keys, and in the allowedCredentials, I don't have transports. I am making progress, but now in the console I get "The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission" - any ideas? I will add my options to my question in case it helps. Thanks for your help so far. – user1130176 May 14 '20 at 16:47
  • Those are all optional so that shouldn't be a problem. It's got to be something to do with how those base64url values are being converted to ArrayBuffers. Is that error returned directly from navigator.credentials.get? – mackie May 14 '20 at 17:05
  • yes, it's actually in the catch block on that line navigator.credentails.get, i actually updated my code to match yours, let me update my question with the new code, maybe something is obvious – user1130176 May 14 '20 at 19:06
  • ok i've updated the code with a comment to show you in the catch block where the error occurs. Super grateful for your help with this. – user1130176 May 14 '20 at 19:12
  • It all looks OK to me, very strange! – mackie May 15 '20 at 18:33