Skip to main content

Authentication with Web Application

In this use-case, FIDO2 Authentication is driven by the web application itself: the application hosts the FIDO2 client JavaScript and calls the nevisFIDO FIDO2 HTTP API directly (through nevisProxy). This is useful for step-up or re-authentication of an already logged-in user before a privileged action, using a passkey the user registered with Nevis.

This is the authentication counterpart of Registration with Web Application. For the nevisAuth-driven login flow (where nevisAuth and nevisLogrend render the page and establish the session), see Authentication instead.

note

The credentials are stored in nevisIDM. This guide covers the Nevis Identity Suite (on-prem) stack (nevisProxy, nevisFIDO, nevisIDM); it does not apply to the Authentication Cloud.

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.
  • The application backend verifies the outcome before granting access.
  1. The user triggers a privileged action in the web application that requires step-up authentication.

  2. The web application calls into the FIDO2 Authentication Client JavaScript.

  3. The FIDO2 Authentication Client JavaScript posts a ServerPublicKeyCredentialGetOptionsRequest (the user's username and the desired userVerification) to the Authentication Options Service of nevisFIDO, which is exposed by nevisProxy.

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

    Reference: nevisFIDO Reference Guide

  4. nevisProxy delegates the SecToken into the request, so nevisFIDO can bind the ceremony to the logged-in user.

  5. nevisFIDO maps the username to a user in nevisIDM and queries the FIDO2 credentials of the user.

  6. A challenge is generated, a FIDO2 session is created, and the ServerPublicKeyCredentialGetOptionsResponse (including the proprietary fido2SessionId) is returned.

  7. The FIDO2 Authentication Client JavaScript receives the response and starts authentication at the browser's WebAuthn API.

  8. The browser asks the user to confirm with the authenticator (user verification).

  9. The user authorizes; the authenticator signs the challenge with the private key.

  10. The WebAuthn API returns the assertion to the FIDO2 Authentication Client JavaScript.

  11. The FIDO2 Authentication Client JavaScript posts the assembled ServerPublicKeyCredential to the Authentication Service of nevisFIDO.

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

    Reference: nevisFIDO Reference Guide

  12. nevisFIDO finds the session by the challenge contained in the assertion, validates the assertion, updates the credential's signature counter in nevisIDM, and updates the session status.

  13. A ServerResponse is returned stating the status of the ceremony together with the fido2SessionId.

  14. The FIDO2 Authentication Client JavaScript forwards the fido2SessionId to the web application backend.

  15. The web application backend verifies the outcome server-to-server by calling the Status Service of nevisFIDO with the fido2SessionId.

    Endpoint: https://<nevisFIDO-host>:<nevisFIDO-port>/nevisfido/fido2/status

    Reference: nevisFIDO Reference Guide

  16. On status succeeded (for the expected userId), the web application authorizes the privileged action and lets the user proceed.

Integration

Overview

The following diagram illustrates the integrated flow and the main points of configuration.

General Considerations

  • The user must already be authenticated at the web application; this use-case covers step-up / re-authentication, not the initial login.
  • The username sent to nevisFIDO must match the credential-repository.user-attribute configured in nevisFIDO (for example loginId or extId).
  • Every time 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 set up in some configuration. It is also assumed that the web application of the relying party is operational, is protected by Nevis, and that the user already has a FIDO2 credential registered (see Registration with Web Application).

Integrate FIDO2 Authentication with a Web Application

  1. Copy the FIDO2 Authentication Client JavaScript and integrate it into your web application as you see fit.

    Browser compatibility

    The example below uses the browser's native WebAuthn JSON APIs (PublicKeyCredential.parseRequestOptionsFromJSON() and PublicKeyCredential.prototype.toJSON()), so no external base64url/encoding library is required. These APIs are available in Chrome 129+, Firefox 119+, and Safari 18.4+. To support older browsers, use a convenience wrapper such as @github/webauthn-json or SimpleWebAuthn, which perform the same base64url conversions.

    FIDO2 Authentication Client JavaScript
    fido2_authentication.js
    function isWebAuthnSupportedByTheBrowser() {
    if (!window.PublicKeyCredential || typeof window.PublicKeyCredential !== "function") {
    console.error("This browser does not support WebAuthn.");
    return false;
    }
    if (typeof PublicKeyCredential.parseRequestOptionsFromJSON !== "function"
    || typeof PublicKeyCredential.prototype.toJSON !== "function") {
    console.error("This browser does not support the WebAuthn JSON APIs. Please use Chrome 129+, Firefox 119+, or Safari 18.4+.");
    return false;
    }
    return true;
    }

    async function assertion(publicKey) {
    const credential = await navigator.credentials.get({ publicKey });
    if (!credential) {
    throw new Error("FIDO2 authentication was canceled or no credential was returned.");
    }

    // toJSON() serializes the assertion with base64url-encoded fields.
    // Send only the fields nevisFIDO expects.
    const json = credential.toJSON();
    const requestBody = {
    type: json.type,
    id: json.id,
    response: {
    clientDataJSON: json.response.clientDataJSON,
    authenticatorData: json.response.authenticatorData,
    signature: json.response.signature,
    userHandle: json.response.userHandle
    },
    userAgent: navigator.userAgent
    };

    const response = await fetch("/nevisfido/fido2/assertion/result", {
    method: "POST",
    headers: { "Accept": "application/json", "Content-Type": "application/json" },
    body: JSON.stringify(requestBody)
    });
    const result = await response.json();
    if (result.status !== "ok") {
    throw new Error(`FIDO2 authentication failed: ${result.errorMessage}`);
    }
    return result.fido2SessionId;
    }

    async function authenticate(username) {
    if (!isWebAuthnSupportedByTheBrowser()) {
    return;
    }

    // Use "required" to force user verification for a step-up; adapt as needed.
    const request = { username, userVerification: "required" };

    try {
    const optionsResponse = await fetch("/nevisfido/fido2/attestation/options", {
    method: "POST",
    headers: { "Accept": "application/json", "Content-Type": "application/json" },
    body: JSON.stringify(request)
    });
    const options = await optionsResponse.json();
    if (options.status && options.status !== "ok") {
    throw new Error(`Could not obtain FIDO2 options: ${options.errorMessage}`);
    }

    // The native API converts the base64url fields (challenge, allowCredentials[].id) for us.
    const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(options);
    const fido2SessionId = await assertion(publicKey);

    // Hand the fido2SessionId to your application backend, which MUST verify it
    // server-to-server against the nevisFIDO status service before granting access.
    return fido2SessionId;
    } catch (error) {
    console.error("Error during FIDO2 authentication:", error);
    }
    }

    Call authenticate(username) from your own scripts when a privileged action requires step-up. The returned fido2SessionId must be handed to your application backend for verification (see the next step).

  2. Verify the authentication.

    The response the browser receives cannot be trusted on its own. The outcome must be confirmed on a trusted channel. Choose the approach that fits your use-case:

    • Application-level step-up (shown above). The web application backend calls the nevisFIDO Status Service server-to-server with the fido2SessionId and only grants the privileged action when the status is succeeded for the expected user.

      Verify the FIDO2 session from your backend
      Verify via the nevisFIDO Status Service
      curl -X POST 'https://<nevisFIDO-host>:<nevisFIDO-port>/nevisfido/fido2/status' \
      -H 'Content-Type: application/json' \
      -d '{ "fido2SessionId": "<fido2SessionId>" }'

      A successful response looks like:

      {
      "status": "succeeded",
      "fido2SessionId": "<fido2SessionId>",
      "timestamp": "2026-07-06T11:31:18.987Z",
      "userId": "<user identifier>"
      }

      Possible status values are initialized, succeeded, failed, cancelled and unknown. Grant access only on succeeded, and check that the returned user matches the logged-in user.

      note

      Perform this call from your backend over a direct server-to-server connection to nevisFIDO, never from the browser: a client could otherwise report a success that did not happen. Make sure the Status Service is reachable only by your backend and is not exposed to end users.

    • Establish a Nevis session / proxy-enforced step-up. If the privileged resource is protected by nevisProxy and you need nevisAuth to raise the authentication level (issue a SecToken), let the client send the fido2SessionId in the nevis-fido2-session-id header to a Fido2AuthState step instead of verifying in your own backend. nevisAuth then verifies the ceremony against the Status Service, reaches AuthDone, and issues the SecToken. This is the mechanism used by the Authentication use-case; see the Fido2AuthState reference.

  3. Configure nevisProxy.

    The FIDO2 calls of the client JavaScript must come through nevisProxy. If there is none yet, create a connector 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/assertion/result</url-pattern>
    </servlet-mapping>

    The /nevisfido/fido2/status endpoint is not mapped here on purpose: it is called by your application backend server-to-server (see step 2), not by the browser.

    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>

    Delegating the SecToken on the options endpoint lets nevisFIDO bind the ceremony to the already logged-in user, which prevents a logged-in user from requesting options for someone else. To enforce it, enable SecToken authorization for authentication in nevisFIDO (see the next step).

  4. Configure FIDO2 at nevisFIDO. Ensure a nevisIDM credential repository is configured (credential-repository.type: nevisidm) and, if you delegate the SecToken as above, enable authentication authorization:

    /var/opt/nevisfido/default/conf/nevisfido.yml
    fido2:
    authorization:
    authentication:
    type: sectoken
  5. Configure nevisIDM.

  6. Restart the instances. FIDO2 Authentication from your web application should now be operational.