14.2.1.2.23. WebAuthn Token

WebAuthn is the Web Authentication API specified by the FIDO Alliance. The register and authentication process is described here:

https://w3c.github.io/webauthn/#sctn-rp-operations

But you do not need to be aware of this. eduMFA wraps all FIDO specific communication, which should make it easier for you, to integrate the U2F tokens managed by eduMFA into your application.

WebAuthn tokens can be either

  • registered by administrators for users or

  • registered by the users themselves.

Be aware that WebAuthn tokens can only be used if the eduMFA server and the applicaedumfations and services the user needs to access all reside under the same domain or subdomains thereof.

This means a WebAuthn token registered by mfa.mycompany.com can be used to sign in to sites like mycompany.com and vpn.mycompany.com, but not (for example) mycompany.someservice.com.

14.2.1.2.23.1. Enrollment

The enrollment/registering can be completely performed within eduMFA.

But if you want to enroll the WebAuthn token via the REST API you need to do it in two steps:

Step 1

POST /token/init HTTP/1.1
Host: <eduMFA server>
Accept: application/json

type=webauthn
user=<username>

The request returns:

HTTP/1.1 200 OK
Content-Type: application/json

{
    "detail": {
        "serial": "<serial number>",
        "webAuthnRegisterRequest": {
            "attestation": "direct",
            "authenticatorSelection": {
                "userVerification": "preferred"
            },
            "displayName": "<user.resolver@realm>",
            "message": "Please confirm with your WebAuthn token",
            "name": "<username>",
            "nonce": "<nonce>",
            "pubKeyCredAlgorithms": [
                {
                    "alg": -7,
                    "type": "public-key"
                },
                {
                    "alg": -37,
                    "type": "public-key"
                }
            ],
            "relyingParty": {
                "id": "<relying party ID>",
                "name": "<relying party name>"
            },
            "serialNumber": "<serial number>",
            "timeout": 60000,
            "transaction_id": "<transaction ID>"
        }
    },
    "result": {
        "status": true,
        "value": true
    },
    "version": "<eduMFA version>"
}

This step returns a webAuthnRegisterRequest which contains a nonce, a relying party (containing a name and an ID generated from your domain), a serial number along with a transaction ID and a message to display to the user. It will also contain some additional options regarding timeout, which authenticators are acceptable, and what key types are acceptable to the server.

With the received data You need to call the javascript function

navigator
    .credentials
    .create({
        challenge: <nonce>,
        rp: <relyingParty>,
        user: {
            id: Uint8Array.from(<serialNumber>, c => c.charCodeAt(0)),
            name: <name>,
            displayName: <displayName>
        },
        pubKeyCredParams: <pubKeyCredAlgorithms>,
        authenticatorSelection: <authenticatorSelection>,
        timeout: <timeout>,
        attestation: <attestation>,
        extensions: {
            authnSel: <authenticatorSelectionList>
        }
    })
    .then(function(credential) { <responseHandler> })
    .catch(function(error) { <errorHandler> });

Here nonce, relyingParty, serialNumber, pubKeyCredAlgorithms, authenticatorSelection, timeout, attestation, authenticatorSelectionList, name, and displayName are the values provided by the server in the webAuthnRegisterRequest field in the response from the first step. authenticatorSelection, timeout, attestation, and authenticatorSelectionList are optional. If attestation is not provided, the client should default to direct attestation. If timeout is not provided, it may be omitted, or a sensible default chosen. Any other optional values must be omitted, if the server has not sent them. Please note that the nonce will be a binary, encoded using the web-safe base64 algorithm specified by WebAuthn, and needs to be decoded and passed as Uint8Array.

If an authenticationSelectionList was given, the responseHandler needs to verify, that the field authnSel of credential.getExtensionResults() contains true. If this is not the case, the responseHandler should abort and call the errorHandler, displaying an error telling the user to use his company-provided token.

The responseHandler needs to then send the clientDataJSON, attestationObject, and registrationClientExtensions contained in the response field of the credential back to the server. If enrollment succeeds, the server will send a response with a webAuthnRegisterResponse field, containing a subject field with the description of the newly created token.

Step 2

POST /token/init HTTP/1.1
Host: <eduMFA server>
Accept: application/json

type=webauthn
transaction_id=<transaction_id>
description=<description>
clientdata=<clientDataJSON>
regdata=<attestationObject>
registrationclientextensions=<registrationClientExtensions>

The values clientDataJSON and attestationObject are returned by the WebAuthn authenticator. description is an optional description string for the new token.

The server expects the clientDataJSON and attestationObject encoded as web-safe base64 as defined by the WebAuthn standard. This encoding is similar to standard base64, but ‘-’ and ‘_’ should be used in the alphabet instead of ‘+’ and ‘/’, respectively, and any padding should be omitted.

The registrationClientExtensions are optional and should simply be omitted, if the client does not provide them. If the registrationClientExtensions are available, they must be encoded as a utf-8 JSON string, then sent to the server as web-safe base64.

Please beware that the btoa() function provided by ECMA-Script expects a 16-bit encoded string where all characters are in the range 0x0000 to 0x00FF. The attestationObject contains CBOR-encoded binary data, returned as an ArrayBuffer.

The problem and ways to solve it are described in detail in this MDN-Article:

https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem

14.2.1.2.23.2. Authentication

The WebAuthn token is a challenge response token. I.e. you need to trigger a challenge, either by sending the OTP PIN/Password for this token to the /validate/check endpoint, or by calling the /validate/triggerchallenge endpoint using a service account with sufficient permissions.

14.2.1.2.23.2.1. Get the challenge (using /validate/check)

The /validate/check endpoint can be used to trigger a challenge using the PIN for the token (without requiring any special permissions).

Request:

POST /validate/check HTTP/1.1
Host: <eduMFA server>
Accept: application/json

user=<username>
pass=<password>

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
    "detail": {
        "attributes": {
            "hideResponseInput": true,
            "img": "<image URL>",
            "webAuthnSignRequest": {
                "allowCredentials": [
                    {
                        "id": "<credential ID>",
                        "transports": [
                            "<allowed transports>"
                        ],
                        "type": "<credential type>"
                    }
                ],
                "challenge": "<nonce>",
                "rpId": "<relying party ID>",
                "timeout": 60000,
                "userVerification": "<user verification requirement>"
            }
        },
        "client_mode": "webauthn",
        "message": "Please confirm with your WebAuthn token",
        "serial": "<token serial>",
        "transaction_id": "<transaction ID>",
        "type": "webauthn"
    },
    "id": 1,
    "jsonrpc": "2.0",
    "result": {
        "authentication": "CHALLENGE",
        "status": true,
        "value": false
    },
    "version": "<eduMFA version>"
}

14.2.1.2.23.2.2. Get the challenge (using /validate/triggerchallenge)

The /validate/triggerchallenge endpoint can be used to trigger a challenge using a service account (without requiring the PIN for the token).

Request

POST /validate/triggerchallenge HTTP/1.1
Host: <eduMFA server>
Accept: application/json
Authorization: <authToken>

user=<username>
serial=<tokenSerial>

Providing the tokenSerial is optional. If just a user is provided, a challenge will be triggered for every challenge response token the user has.

Response

HTTP/1.1 200 OK
Content-Type: application/json

{
    "detail": {
        "attributes": {
            "hideResponseInput": true,
            "img": "<image URL>",
            "webAuthnSignRequest": {
                "challenge": "<nonce>",
                "allowCredentials": [{
                    "id": "<credential ID>",
                    "transports": [
                        "<allowed transports>"
                    ],
                    "type": "<credential type>",
                }],
                "rpId": "<relying party ID>",
                "userVerification": "<user verification requirement>",
                "timeout": 60000
            }
        },
        "message": "Please confirm with your WebAuthn token",
        "messages": ["Please confirm with your WebAuthn token"],
        "multi_challenge": [{
            "attributes": {
                "hideResponseInput": true,
                "img": "<image URL>",
                "webAuthnSignRequest": {
                    "challenge": "<nonce>",
                    "allowCredentials": [{
                        "id": "<credential ID>",
                        "transports": [
                            "<allowedTransports>"
                        ],
                        "type": "<credential type>",
                    }],
                    "rpId": "<relying party ID>",
                    "userVerification": "<user verification requirement>",
                    "timeout": 60000
                }
            },
            "message": "Please confirm with your WebAuthn token",
            "serial": "<token serial>",
            "transaction_id": "<transaction ID>",
            "type": "webauthn"
        }],
        "serial": "<token serial>",
        "transaction_id": "<transaction ID>",
        "transaction_ids": ["<transaction IDs>"],
        "type": "webauthn"
    },
    "id": 1,
    "jsonrpc": "2.0",
    "result": {
        "status": true,
        "value": 1
    },
    "version": "<eduMFA version>"
}

14.2.1.2.23.2.3. Send the Response

The application now needs to call the javascript function navigator.credentials.get with the publicKeyCredentialRequestOptions built using the nonce, credentialId, allowedTransports, userVerificationRequirement and timeout from the server. The timeout is optional and may be omitted, if not provided, the client may also pick a sensible default. Please note that the nonce will be a binary, encoded using the web-safe base64 algorithm specified by WebAuthn, and needs to be decoded and passed as Uint8Array.

const publicKeyCredentialRequestOptions = {
    challenge: <nonce>,
    allowCredentials: [{
        id: Uint8Array.from(<credentialId>, c=> c.charCodeAt(0)),
        type: <credentialType>,
        transports: <allowedTransports>
    }],
    userVerification: <userVerificationRequirement>,
    rpId: <relyingPartyId>,
    timeout: <timeout>
}
navigator
    .credentials
    .get({publicKey: publicKeyCredentialRequestOptions})
    .then(function(assertion) { <responseHandler> })
    .catch(function(error) { <errorHandler> });

The responseHandler needs to call the /validate/check API providing the serial of the token the user is signing in with, and the transaction_id, for the current challenge, along with the id, returned by the WebAuthn device in the assertion and the authenticatorData, clientDataJSON and signature, userHandle, and assertionClientExtensions contained in the response field of the assertion.

clientDataJSON, authenticatorData and signature should be encoded as web-safe base64 without padding. For more detailed instructions, refer to “2. Step” under “Enrollment” above.

The userHandle and assertionClientExtensions are optional and should be omitted, if not provided by the authenticator. The assertionClientExtensions – if available – must be encoded as a utf-8 JSON string, and transmitted to the server as web-safe base64. The userHandle is simply passed as a string, note – however – that it may be necessary to re-encode this to utf-16, since the authenticator will return utf-8, while the library making the http request will likely require all parameters in the native encoding of the language (usually utf-16).

POST /validate/check HTTP/1.1
Host: example.com
Accept: application/json

user=<user>
pass=
transaction_id=<transaction_id>
credentialid=<id>
clientdata=<clientDataJSON>
signaturedata=<signature>
authenticatordata=<authenticatorData>
userhandle=<userHandle>
assertionclientextensions=<assertionClientExtensions>

14.2.1.2.23.3. Implementation

class edumfa.lib.tokens.webauthntoken.WebAuthnTokenClass(db_token)[source]

The WebAuthn Token implementation.

Create a new WebAuthn Token object from a database object

Parameters:

db_token (DB object) – instance of the orm db object

check_otp(otpval, counter=None, window=None, options=None)[source]

This checks the response of a previous challenge.

Since this is not a traditional token, otpval and window are unused. The information from the client is instead passed in the fields serial, id, assertion, authenticatorData, clientDataJSON, and signature of the options dictionary.

Parameters:
  • otpval (None) – Unused for this token type

  • counter (int) – The authentication counter

  • window (None) – Unused for this token type

  • options (dict) – Contains the data from the client, along with policy configurations.

Returns:

A numerical value where values larger than zero indicate success.

Return type:

int

static check_userless_otp(options=None)[source]
client_mode = 'webauthn'
create_challenge(transactionid=None, options=None)[source]

Create a challenge for challenge-response authentication.

This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database.

If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction.

This method will return a tuple containing a bool value, indicating whether a challenge was successfully created, along with a message to display to the user, the transaction id, and a dictionary containing all parameters and data needed to respond to the challenge, as per the api.

Parameters:
  • transactionid (basestring) – The id of this challenge

  • options (dict) – The request context parameters and data

Returns:

Success status, message, transaction id and reply_dict

Return type:

(bool, basestring, basestring, dict)

static create_usernameless_challenge(options)[source]

Creates a challenge for a username less authentication

Parameters:

options (dict) – The request context parameters and data

Returns:

Success status, message, transaction id and reply_dict

Return type:

(bool, basestring, basestring, dict)

decrypt_otpkey()[source]

This method fetches a decrypted version of the otp_key.

This method becomes necessary, since the way WebAuthn is implemented in eduMFA, the otpkey of a WebAuthn token is the credential_id, which may encode important information and needs to be sent to the client to allow the client to create an assertion for the authentication process.

Returns:

The otpkey decrypted and encoded as WebAuthn base64.

Return type:

basestring

static get_class_info(key=None, ret='all')[source]

returns a subtree of the token definition

Parameters:
  • key (string) – subsection identifier

  • ret (user defined) – default return value, if nothing is found

Returns:

subsection if key exists or user defined

Return type:

dict or scalar

static get_class_prefix()[source]

Return the prefix, that is used as a prefix for the serial numbers.

Returns:

WAN

Return type:

basestring

static get_class_type()[source]

Returns the internal token type identifier

Returns:

webauthn

Return type:

basestring

get_init_detail(params=None, user=None)[source]

At the end of the initialization we ask the user to confirm the enrollment with his token.

This will prepare all the information the client needs to build the publicKeyCredentialCreationOptions to call navigator.credentials.create() with. It will then be called again, once the token is created and provide confirmation of the successful enrollment to the client.

Parameters:
  • params (dict) – A dictionary with parameters from the request.

  • user (User) – The user enrolling the token.

Returns:

The response detail returned to the client.

Return type:

dict

static get_setting_type(key)[source]

Fetch the type of a setting specific to WebAuthn tokens.

The WebAuthn token defines several public settings. When these are written to the database, the type of the setting is automatically stored along with the setting by set_edumfa_config().

The key name needs to be in WEBAUTHN_TOKEN_SPECIFIC_SETTINGS.keys() and match /^webauthn./. If the specified setting does not exist, a ValueError will be thrown.

Parameters:

key (basestring) – The token specific setting key

Returns:

The setting type

Return type:

“public”

is_challenge_request(passw, user=None, options=None)[source]

Check if the request would start a challenge.

Every request that is not a response needs to spawn a challenge.

Note: This function does not need to be decorated with @challenge_response_allowed, as the WebAuthn token is always a challenge response token!

Parameters:
  • passw (basestring) – The PIN of the token

  • user (User) – The User making the request

  • options (dict) – Dictionary of additional request parameters

Returns:

Whether to trigger a challenge

Return type:

bool

update(param, reset_failcount=True)[source]

This method is called during the initialization process.

Parameters:
  • param (dict) – Parameters from the token init.

  • reset_failcount (bool) – Whether to reset the fail count.

Returns:

Nothing

Return type:

None