15.2.1.2.22. 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. privacyIDEA wraps all FIDO specific communication, which should make it easier for you, to integrate the U2F tokens managed by privacyIDEA 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 privacyIDEA server and the applications and services the user needs to access all reside under the same domain or subdomains thereof.

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

15.2.1.2.22.1. Enrollment

The enrollment/registering can be completely performed within privacyIDEA.

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: <privacyIDEA 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": "<privacyIDEA 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: <privacyIDEA 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

15.2.1.2.22.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.

15.2.1.2.22.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: <privacyIDEA 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": "<privacyIDEA version>"
}

15.2.1.2.22.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: <privacyIDEA server>
Accept: application/json
PI-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": "<privacyIDEA version>"
}

15.2.1.2.22.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>

15.2.1.2.22.3. Implementation

class privacyidea.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

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)

decrypt_otpkey()[source]

This method fetches a decrypted version of the otp_key.

This method becomes necessary, since the way WebAuthn is implemented in PrivacyIdea, 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_privacyidea_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