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 prerequisites, 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.
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 user accesses a Web Application after authenticating at Nevis with a legacy credential (a password for example).
The Web Application loads into the browser, with it the FIDO2 Registration Client Javascript.
The user initiates FIDO2 Registration on the GUI of the Web Application.
The Web Application calls into the FIDO2 Registration Client Javascript.
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
nevisProxy delegates the SecToken into the request, which then will be parsed by nevisFIDO.
nevisFIDO maps the username into a user in nevisIDM, then queries the credentials of the user.
Challenge is generated and the
ServerPublicKeyCredentialCreationOptionsResponse
is built, then returned.The FIDO2 Registration Client Javascript receives the
ServerPublicKeyCredentialCreationOptionsResponse
and initiates registration with it at the Browser's WebAuthn API.Dialog presented to the user by the browser to confirm the credential creation.
The user approves the credential creation.
The WebAuthn API generates the keys and returns an attestation to the FIDO2 Registration Client Javascript.
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
nevisFIDO session lookup.
Incoming request validated according to the WebAuthn specification.
nevisFIDO stores the FIDO2 credential in nevisIDM, such that it is related to the username.
FIDO2 session is updated to reflect the current status.
ServerResponse
is returned stating the status of the FIDO2 ceremony. At this point the FIDO2 registration is completed.The FIDO2 Registration Client Javascript logs the errors if necessary.
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.
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
Copy the Javascript files and integrate them into your web application as you see fit.
FIDO2 Registration Client Javascript
fido2_registration.jsasync 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.username
: the nature of this property must match tocredential-repository.user-attribute
in the nevisFIDO configuration. For example, ifcredential-repository.user-attribute = loginId
is configured, theusername
property must contain theloginId
of the user, as known by nevisIDM.displayName
: this property should match to thedisplayName
property the SCIM API List of Users returns from nevisIDM.
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>Restart the instances, FIDO2 Registration should be operational!