Webauthn - Server side libraries
Tags: auth, webauthn, passwordless
Published: 2022-03-13 20:20:42

Since manual validation/verification of "attestations", which contain the authenticity proof of the payload, let's check the ecosystem and look if good libraries are available. As a reminder, the attestation can be encoded into various formats, each containing contain some form of cryptographic signature. Therefore, it is possible that a library support some of these formats and not others.

When looking for promising libraries on gihub, the following four appear to be the most popular:

| Language | Stars | License | Link
|----------|-------|---------|------
| GO | 750+ | BSD-3 | https://github.com/duo-labs/webauthn
| Python | 380+ | BSD-3 | https://github.com/duo-labs/py_webauthn
| Java | 200+ | Apache-2 | https://github.com/webauthn4j/webauthn4j
| PHP | 120+ | MIT | https://github.com/lbuchs/WebAuthn

Of course, there are mony more libraries. This is just a little pick to illustrate that the ecosystem is already there, although young, and that a variety of programming languages have libraries available.

Python example
--------------

This will not be a full example with a webserver, but rather a snippet to test the decoding and validation using the previously mentioned library.

Let us start with some JSON similar to the one produced in the previous tutorial of this series. Namely, the same format of the normal response of `navigator.credentials.create(...)` with all byte buffers directly encoded as base64url.

```json
{
    "id": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
    "rawId": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
    "response": {
        "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBZ0mWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAAAAAAAAAAAAAAAAAAAAAAAACBmggo_UlC8p2tiPVtNQ8nZ5NSxst4WS_5fnElA2viTq6QBAwM5AQAgWQEA31dtHqc70D_h7XHQ6V_nBs3Tscu91kBL7FOw56_VFiaKYRH6Z4KLr4J0S12hFJ_3fBxpKfxyMfK66ZMeAVbOl_wemY4S5Xs4yHSWy21Xm_dgWhLJjZ9R1tjfV49kDPHB_ssdvP7wo3_NmoUPYMgK-edgZ_ehttp_I6hUUCnVaTvn_m76b2j9yEPReSwl-wlGsabYG6INUhTuhSOqG-UpVVQdNJVV7GmIPHCA2cQpJBDZBohT4MBGme_feUgm4sgqVCWzKk6CzIKIz5AIVnspLbu05SulAVnSTB3NxTwCLNJR_9v9oSkvphiNbmQBVQH1tV_psyi9HM1Jtj9VJVKMeyFDAQAB",
        "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ2VUV29nbWcwY2NodWlZdUZydjhEWFhkTVpTSVFSVlpKT2dhX3hheVZWRWNCajBDdzN5NzN5aEQ0RmtHU2UtUnJQNmhQSkpBSW0zTFZpZW40aFhFTGciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9"
    },
    ...
}
```

The exact content might differ depending on where/how the registration was performed, but the above content is what matters. Let's parse it first.

```python
import webauthn

parsed_credential = webauthn.helpers.structs.RegistrationCredential.parse_raw(json)
```

Then, validate it using the following method.

```python
registration = webauthn.verify_registration_response(
    credential=parsed_credential,
    expected_challenge=webauthn.base64url_to_bytes("CeTWogmg0cchuiYuFrv8DXXdMZSIQRVZJOga_xayVVEcBj0Cw3y73yhD4FkGSe-RrP6hPJJAIm3LVien4hXELg"),
    expected_origin="http://localhost:5000",
    expected_rp_id="localhost"
)
```

That is the bare minimum. First, this validates the content, including the signature, but also provides the public key, used later to validate authentication requests.

```python
print('Credential ID:', webauthn.helpers.bytes_to_base64url(registration.credential_id))
print('Public Key:', webauthn.helpers.bytes_to_base64url(registration.credential_public_key))
```

The authentication works similarly. Here as well, we will take the input of `navigator.credentials.get(...)` with all byte buffers directly encoded as base64url.

```python
parsed_credential = webauthn.helpers.structs.AuthenticationCredential.parse_raw('''
{
    "id": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
    "rawId": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
    "response": {
        "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ",
        "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVBtQWkxUHAxWEw2b0FncTNQV1p0WlBuWmExekZVRG9HYmFRMF9LdlZHMWxGMnMzUnRfM280dVN6Y2N5MHRtY1RJcFRUVDRCVTFULUk0bWFhdm5kalEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9",
        "signature": "iOHKX3erU5_OYP_r_9HLZ-CexCE4bQRrxM8WmuoKTDdhAnZSeTP0sjECjvjfeS8MJzN1ArmvV0H0C3yy_FdRFfcpUPZzdZ7bBcmPh1XPdxRwY747OrIzcTLTFQUPdn1U-izCZtP_78VGw9pCpdMsv4CUzZdJbEcRtQuRS03qUjqDaovoJhOqEBmxJn9Wu8tBi_Qx7A33RbYjlfyLm_EDqimzDZhyietyop6XUcpKarKqVH0M6mMrM5zTjp8xf3W7odFCadXEJg-ERZqFM0-9Uup6kJNLbr6C5J4NDYmSm3HCSA6lp2iEiMPKU8Ii7QZ61kybXLxsX4w4Dm3fOLjmDw"
    }
}
''')

authentication_verification = webauthn.verify_authentication_response(
    credential=parsed_credential,
    expected_challenge=webauthn.base64url_to_bytes("iPmAi1Pp1XL6oAgq3PWZtZPnZa1zFUDoGbaQ0_KvVG1lF2s3Rt_3o4uSzccy0tmcTIpTTT4BU1T-I4maavndjQ"),
    expected_rp_id="localhost",
    expected_origin="http://localhost:5000",
    credential_public_key=registration.credential_public_key,
    credential_current_sign_count=0,
    require_user_verification=True,
)
```

Before closing the topic I would like to take a step back and look at the big picture again. Although the user ID and name is *required* during the creation of credentials, it is not present in the response at all, nor used in the authentication process afterwards ...this asymmetry is kind of bizarre IMHO. Perhaps the authenticator uses it somehow for display purposes for things like "sub accounts" ...it's not very transparent.

This specification is also "in flux", and there are for example plans to add comfort functions to provide json obects directly. See https://github.com/w3c/webauthn/pull/1703 . Nevertheless, the complexity of the whole remains.

In the next series, we will discuss how this complex functionality could be offered more conviniently.