Skip to main content

Registration with Web Application

In this use-case, the FIDO2 Registration is integrated into the web application, where the FIDO2 credentials will be used at. For a generic description and prerequisits, please see Registration.

Technical Flow

The initial authentication depends on the custom integration as well as the existing means of authentication the end user possesses. Thus, the initial authentication steps are not explained in detail but only referenced.

note

The following flow is a simple compact example. Depending on requirements, changes or different approaches might be required. The key fix points are:

  • WebAuthn API in the browser.
  • FIDO2 HTTP API in nevisFIDO.
  1. The user accesses a Web Application after authenticating at Nevis with a legacy credential (a password for example).

  2. The Web Application loads into the browser, with it the FIDO2 Registration Client Javascript.

  3. The user initiates FIDO2 Registration on the GUI of the Web Application.

  4. The Web Application calls into the FIDO2 Registration Client Javascript.

  5. The FIDO2 Registration Client Javascript posts the ServerPublicKeyCredentialCreationOptionsRequest to the Registration Options Service of nevisFIDO, which is exposed by nevisProxy.

    Endpoint: https://<nevisProxy-host>:<nevisProxy-port>/nevisfido/fido2/attestation/options

    Reference: nevisFIDO Reference Guide

  6. nevisProxy delegates the SecToken into the request, which then will be parsed by nevisFIDO.

  7. nevisFIDO maps the username into a user in nevisIDM, then queries the credentials of the user.

  8. Challenge is generated and the ServerPublicKeyCredentialCreationOptionsResponse is built, then returned.

  9. The FIDO2 Registration Client Javascript receives the ServerPublicKeyCredentialCreationOptionsResponse and initiates registration with it at the Browser's WebAuthn API.

  10. Dialog presented to the user by the browser to confirm the credential creation.

  11. The user approves the credential creation.

  12. The WebAuthn API generates the keys and returns an attestation to the FIDO2 Registration Client Javascript.

  13. IDO2 Registration Client Javascript assembles then posts a ServerPublicKeyCredential to the nevisFIDO REST API that is exposed by nevisProxy.

    Endpoint: https://<nevisProxy-host>:<nevisProxy-port>/nevisfido/fido2/attestation/result

    Reference: nevisFIDO Reference Guide

  14. nevisFIDO session lookup.

  15. Incoming request validated according to the WebAuthn specification.

  16. nevisFIDO stores the FIDO2 credential in nevisIDM, such that it is related to the username.

  17. FIDO2 session is updated to reflect the current status.

  18. ServerResponse is returned stating the status of the FIDO2 ceremony. At this point the FIDO2 registration is completed.

  19. The FIDO2 Registration Client Javascript logs the errors if necessary.

  20. The FIDO2 Registration Client Javascript returns to the Web Application, which may proceed.

Integration

Overview

The following diagram illustrates the integrated flow, as well as the main points of configuration.

FIDO2 Registration with web application

General Considerations

  • A legacy authentication should preceed FIDO2 Registration, which ensures the user is authenticated before it can create new credentials.
  • Everytime you copy and create a file, make sure it is readable by nvauser, and is thus accessible by Nevis components.

Disclaimer

The guide assumes the Nevis components nevisProxy, nevisFIDO and nevisIDM are already installed and setup in some configuration. It is also assumed that the web application of the relying party is operational and is protected by Nevis.

Integrate FIDO2 Registration with a Web Application

  1. Copy the Javascript files and integrate them into your web application as you see fit.

    FIDO2 Registration Client Javascript
    fido2_registration.js
    async function attestation(options) {
    try {
    const credential = await navigator.credentials.create({
    "publicKey": options
    });
    const requestBody = {
    id: credential.id,
    type: credential.type,
    response: {
    clientDataJSON: base64url.encode(credential.response.clientDataJSON),
    attestationObject: base64url.encode(credential.response.attestationObject)
    }
    };

    await fetch("/nevisfido/fido2/attestation/result", {
    method: "POST",
    headers: {
    "Content-Type": "application/json"
    },
    body: JSON.stringify(requestBody)
    });
    } catch (error) {
    console.error("Error while submitting WebAuthn attestation:", error);
    }
    }

    async function register(username, displayName) {
    if (!username) {
    console.error("Invalid username");
    }
    if (!displayName) {
    console.error("Invalid displayName");
    }
    if (!isWebAuthnSupportedByTheBrowser()) {
    console.error("WebAuthn is not supported by the browser!");
    return;
    }

    const request = {
    username,
    displayName,
    authenticatorSelection: {
    residentKey: "required",
    authenticatorAttachment: "platform",
    userVerification: "required"
    },
    attestation: "direct"
    };

    try {
    const response = await fetch("/nevisfido/fido2/attestation/options", {
    method: "POST",
    headers: {
    "Content-Type": "application/json"
    },
    body: JSON.stringify(request)
    });

    const options = await response.json();
    options.user.id = base64url.decode(options.user.id);
    options.challenge = base64url.decode(options.challenge);
    if (options.excludeCredentials != null) {
    options.excludeCredentials = options.excludeCredentials.map(({
    id,
    ...rest
    }) => ({
    ...rest,
    id: base64url.decode(id)
    }));
    }
    if (options.authenticatorSelection.authenticatorAttachment === null) {
    options.authenticatorSelection.authenticatorAttachment = undefined;
    }
    await attestation(options);
    } catch (error) {
    console.error("Error during FIDO2 registration:", error);
    }
    }
    Base64url encoding library
    /var/opt/nevislogrend/<instance>/data/applications/def/resources/base64url.js
    /*
    * Base64URL-ArrayBuffer
    * https://github.com/herrjemand/Base64URL-ArrayBuffer
    *
    * Copyright (c) 2017 Yuriy Ackermann <[email protected]>
    * Copyright (c) 2012 Niklas von Hertzen
    * Licensed under the MIT license.
    *
    */
    (function() {
    "use strict";

    var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

    // Use a lookup table to find the index.
    var lookup = new Uint8Array(256);
    for (var i = 0; i < chars.length; i++) {
    lookup[chars.charCodeAt(i)] = i;
    }

    var encode = function(arraybuffer) {
    var bytes = new Uint8Array(arraybuffer),
    i, len = bytes.length, base64 = "";

    for (i = 0; i < len; i+=3) {
    base64 += chars[bytes[i] >> 2];
    base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
    base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
    base64 += chars[bytes[i + 2] & 63];
    }

    if ((len % 3) === 2) {
    base64 = base64.substring(0, base64.length - 1);
    } else if (len % 3 === 1) {
    base64 = base64.substring(0, base64.length - 2);
    }

    return base64;
    };

    var decode = function(base64) {
    var bufferLength = base64.length * 0.75,
    len = base64.length, i, p = 0,
    encoded1, encoded2, encoded3, encoded4;

    var arraybuffer = new ArrayBuffer(bufferLength),
    bytes = new Uint8Array(arraybuffer);

    for (i = 0; i < len; i+=4) {
    encoded1 = lookup[base64.charCodeAt(i)];
    encoded2 = lookup[base64.charCodeAt(i+1)];
    encoded3 = lookup[base64.charCodeAt(i+2)];
    encoded4 = lookup[base64.charCodeAt(i+3)];

    bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
    bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
    bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
    }

    return arraybuffer;
    };

    /**
    * Exporting and stuff
    */
    if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
    module.exports = {
    'encode': encode,
    'decode': decode
    }

    } else {
    if (typeof define === 'function' && define.amd) {
    define([], function() {
    return {
    'encode': encode,
    'decode': decode
    }
    });
    } else {
    window.base64url = {
    'encode': encode,
    'decode': decode
    }
    }
    }
    })();

    Adapt FIDO2 Registration Client Javascript as you see fit, particularly check and adapt error handling and the options properties: residentKey, authenticatorAttachment, userVerification, attestation. If undecided, we recommend leaving everything on default, which requires the client-side to create a discoverable platform credential with the user's explicit verification.

    Call register(username, displayName) from your own scripts as you see fit, this method executes FIDO2 Registration against Nevis.

    1. username: the nature of this property must match to credential-repository.user-attribute in the nevisFIDO configuration. For example, if credential-repository.user-attribute = loginId is configured, the username property must contain the loginId of the user, as known by nevisIDM.
    2. displayName: this property should match to the displayName property the SCIM API List of Users returns from nevisIDM.
  2. Configure nevisProxy.

    The FIDO2 calls of the FIDO2 Registration Client Javascript now must come through nevisProxy, where the SecToken is delegated into the request, so nevisFIDO can authorize the requests properly. If there is none yet, create a connector in nevisProxy towards nevisFIDO.

    Create the nevisFIDO connector with AutoRewrite off
    /var/opt/nevisproxy/default/work/WEB-INF/web.xml
    <servlet>
    <servlet-name>FidoConnector</servlet-name>
    <servlet-class>ch::nevis::isiweb4::servlet::connector::http::HttpsConnectorServlet</servlet-class>
    <init-param>
    <param-name>InetAddress</param-name>
    <param-value>localhost:9443</param-value>
    </init-param>
    <init-param>
    <param-name>AutoRewrite</param-name>
    <param-value>off</param-value>
    </init-param>
    <init-param>
    <param-name>SSLClientCertificateFile</param-name>
    <param-value>/var/opt/keybox/default/node_keystore.pem</param-value>
    </init-param>
    <init-param>
    <param-name>SSLCACertificateFile</param-name>
    <param-value>/var/opt/keybox/default/truststore.pem</param-value>
    </init-param>
    </servlet>

    Replace localhost:9443 with the endpoint nevisFIDO is accessible at in your network.

    nevisFIDO FIDO2 connector mappings
    /var/opt/nevisproxy/default/work/WEB-INF/web.xml
        <servlet-mapping>
    <servlet-name>FidoConnector</servlet-name>
    <url-pattern>/nevisfido/fido2/attestation/options</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
    <servlet-name>FidoConnector</servlet-name>
    <url-pattern>/nevisfido/fido2/attestation/result</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
    <servlet-name>FidoConnector</servlet-name>
    <url-pattern>/nevisfido/fido2/status</url-pattern>
    </servlet-mapping>
    Delegate the SecToken and the UserId for the Options endpoint
    /var/opt/nevisproxy/default/work/WEB-INF/web.xml
        <filter>
    <filter-name>SecTokenDelegationFilter</filter-name>
    <filter-class>::ch::nevis::isiweb4::filter::delegation::DelegationFilter</filter-class>
    <init-param>
    <param-name>DelegateBasicAuth</param-name>
    <param-value>
    AUTH:user.auth.UserId
    AUTH:user.auth.SecToken
    </param-value>
    </init-param>
    </filter>

    <filter-mapping>
    <filter-name>SecTokenDelegationFilter</filter-name>
    <url-pattern>/nevisfido/fido2/attestation/options</url-pattern>
    </filter-mapping>
  3. Configure FIDO2 at nevisFIDO.

  4. Configure nevisIDM.

  5. Restart the instances, FIDO2 Registration should be operational!