duo-labs / py_webauthn

Pythonic WebAuthn 🐍
https://duo-labs.github.io/py_webauthn
BSD 3-Clause "New" or "Revised" License
838 stars 165 forks source link

bytes encoding doesn't work with browsers #103

Closed samuelcolvin closed 2 years ago

samuelcolvin commented 2 years ago

Thanks so much for the library, just what I needed.

Also amazing to see pydantic used in libraries like this :smile:.


I'm having trouble using the output of webauthn.generate_registration_options in the browser, in particular I need ArrayBuffers not strings for challenge and user.id.

The problem is that you're currently encoding strings using urlsafe_b64encode in options_to_json() which is not compatible with atob().

Would you consider switching to plain base64.b64encode, or at least making it configurable?

With that I can using the following to convert strings to ArrayBuffer:

const asArrayBuffer = v => Uint8Array.from(atob(v), c => c.charCodeAt(0))

Alternatively, if you have an elegant way to convert the output of options_to_json to make it suitable for use with navigator.credentials.create() in JS, it might be good to add it to the docs.

jwag956 commented 2 years ago

I use this that I got from the older version of this package (they used to have a complete flask example):

`const transformCredentialCreateOptions = (credentialCreateOptionsFromServer) => { let {challenge, user} = credentialCreateOptionsFromServer; user.id = Uint8Array.from( atob(credentialCreateOptionsFromServer.user.id .replace(/_/g, "/") .replace(/-/g, "+") ), c => c.charCodeAt(0));

challenge = Uint8Array.from(
    atob(credentialCreateOptionsFromServer.challenge
        .replace(/\_/g, "/")
        .replace(/\-/g, "+")
        ),
    c => c.charCodeAt(0));

const transformedCredentialCreateOptions = Object.assign(
        {}, credentialCreateOptionsFromServer,
        {challenge, user});

return transformedCredentialCreateOptions;

}`

MasterKale commented 2 years ago

The spec specifically mentions base64url as a dependency, and the encoding is used throughout. For sake of adherence to the spec I chose to use base64url as well in options_to_json(). You're right that base64url is not browser-JS friendly, and this dependency is one of my dev-focused gripes with the spec.

To tackle this I'd personally recommend using one of a couple of JS libraries that expect base64url and are capable of transforming it prior to triggering WebAuthn API calls:

I'm of course biased for my own library (the second one), and I know for a fact that it works fine with the output from options_to_json(). Here's a gist showing how I combined py_webauthn with @simplewebauthn/browser on the front end to invoke WebAuthn API without needing to juggle an encoding type:

https://gist.github.com/MasterKale/d709e580a063887c33de9b73d509dec1

MasterKale commented 2 years ago

I'm going to close this issue for now assuming no news is good news. Please feel free to reopen if needed.

samuelcolvin commented 2 years ago

Humm, I'm not sure you're right about the spec requiring the use of urlsafe_b64encode

The term Base64url Encoding refers to the base64 encoding using the URL- and filename-safe character set defined in Section 5 of [RFC4648], with all trailing '=' characters omitted (as permitted by Section 3.2) and without the inclusion of any line breaks, whitespace, or other additional characters.

This implies that you should be safe to use base64.b64encode(...).strip(b'=').

Indeed from the above your use of base64.urlsafe_b64encode(...) is explicitly NOT compliant with the above part of the spec since urlsafe_b64encode() includes trailing =.

I think this issue should be reopened.

MasterKale commented 2 years ago

Indeed from the above your use of base64.urlsafe_b64encode(...) is explicitly NOT compliant with the above part of the spec since urlsafe_b64encode() includes trailing =.

That fun fact is why I included a bytes_to_base64url() helper (and it's corresponding base64url_to_bytes which encapsulates/benefits from other quirks about urlsafe_b64decode):

def bytes_to_base64url(val: bytes) -> str:
    """
    Base64URL-encode the provided bytes
    """
    return urlsafe_b64encode(val).decode("utf-8").replace("=", "")
samuelcolvin commented 2 years ago

Thanks, sorry I missed that.

samuelcolvin commented 2 years ago

By the way, you can make this code slightly more performance and expressive with

return urlsafe_b64encode(val).rstrip(b'=').decode()

Since = can only appear at the end of the string.