Integration guide
This guide provides step-by-step instructions for integrating the FIDO2 Web SDK with the IDCloud FIDO2 Server in your web application.
Architecture
Browser (Your Web Application)
- FIDO2 Web SDK
- WebAuthn API (browser native)
- IndexedDB (Device Bound Key storage)
|
| HTTPS
v
Your Relying Party Backend
- REST API endpoints
- Session management
- User database
|
| HTTPS
v
IDCloud FIDO2 Server
- /attestation/options
- /attestation/result
- /assertion/options
- /assertion/result
The SDK runs entirely in the browser. Your Relying Party backend acts as an intermediary between the browser and the FIDO2 Server. The FIDO2 Server is never accessed directly from the browser.
Step 1: Implement the Relying Party backend
Your Relying Party backend must implement four endpoints that proxy requests between the browser and the FIDO2 Server.
Attestation options — POST /api/rp/fido2/attestation/options
Retrieves registration options from the FIDO2 Server.
Request body from browser:
{
"userId": "user123"
}
What the backend must do:
- Receive the user identifier from the request.
- Optionally receive
dbkkidfrom the browser if using the Device Bound Key extension. - Call the FIDO2 Server
POST /attestation/optionsendpoint. - Return the FIDO2 Server response to the browser.
Example FIDO2 Server request:
{
"userId": "base64url_encoded_user_id",
"displayName": "user@example.com",
"extensions": {
"thalesgroup_dbk_ext": {
"v": 1,
"kid": "optional_dbkkid"
}
}
}
Example FIDO2 Server response returned to browser:
{
"challenge": "...",
"rp": { "id": "example.com", "name": "Example RP" },
"user": { "id": "...", "name": "...", "displayName": "..." },
"pubKeyCredParams": [...],
"extensions": {
"thalesgroup_dbk_ext": {
"v": 1,
"kid": "...",
"allowedAlgorithms": [-7]
}
}
}
Attestation result — POST /api/rp/fido2/attestation/result
Verifies the credential with the FIDO2 Server after registration.
Request body from browser: The credential object returned by webauthnCreateCred().
What the backend must do:
- Receive the credential from the request body.
- Retrieve the user identifier from the session or token.
- Call the FIDO2 Server
POST /attestation/result?userId={userId}endpoint. - Return the verification result to the browser.
Assertion options — POST /api/rp/fido2/assertion/options
Retrieves authentication options from the FIDO2 Server.
Request body from browser:
{
"userId": "user123"
}
userId is optional for userless (discoverable credential) flows.
What the backend must do:
- Receive the optional user identifier from the request.
- Call the FIDO2 Server
POST /assertion/optionsendpoint. - Return the FIDO2 Server response to the browser.
Assertion result — POST /api/rp/fido2/assertion/result
Verifies the authentication assertion with the FIDO2 Server.
Request body from browser: The credential object returned by webauthnGetCred().
What the backend must do:
- Receive the credential from the request body.
- Call the FIDO2 Server
POST /assertion/resultendpoint. - Create a user session on success.
- Return the result to the browser.
Backend reference implementation (Node.js/Express)
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const FIDO2_SERVER_URL = 'https://fido2-server.example.com';
async function callFido2Server(path, data) {
const response = await axios.post(`${FIDO2_SERVER_URL}${path}`, data, {
headers: { 'Content-Type': 'application/json' }
});
return response.data;
}
// Attestation options
app.post('/api/rp/fido2/attestation/options', async (req, res) => {
try {
const { userId, dbkkid } = req.body;
const userIdB64 = Buffer.from(userId).toString('base64url');
const options = await callFido2Server('/attestation/options', {
userId: userIdB64,
displayName: `user-${userId}`,
extensions: {
thalesgroup_dbk_ext: { v: 1, kid: dbkkid }
}
});
res.json(options);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Attestation result
app.post('/api/rp/fido2/attestation/result', async (req, res) => {
try {
const credential = req.body;
const userId = req.session.userId;
const result = await callFido2Server(
`/attestation/result?userId=${userId}`,
credential
);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Assertion options
app.post('/api/rp/fido2/assertion/options', async (req, res) => {
try {
const { userId } = req.body;
const requestBody = { extensions: { thalesgroup_dbk_ext: { v: 1 } } };
if (userId) {
requestBody.userId = Buffer.from(userId).toString('base64url');
}
const options = await callFido2Server('/assertion/options', requestBody);
res.json(options);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Assertion result
app.post('/api/rp/fido2/assertion/result', async (req, res) => {
try {
const credential = req.body;
const result = await callFido2Server('/assertion/result', credential);
if (result.success) {
req.session.userId = result.userId;
}
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
See the FIDO authentication APIs for the full FIDO2 Server endpoint reference.
Step 2: Frontend integration
HTML setup
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FIDO2 Authentication</title>
</head>
<body>
<section>
<h2>Registration</h2>
<input type="text" id="regUserId" placeholder="User ID">
<button id="registerBtn">Register</button>
<div id="regStatus"></div>
</section>
<section>
<h2>Authentication</h2>
<input type="text" id="authUserId" placeholder="User ID (optional)">
<button id="authenticateBtn">Authenticate</button>
<div id="authStatus"></div>
</section>
<script src="/lib/idc-fido2-web-js-lib-bundle.js"></script>
<script src="/js/fido2-integration.js"></script>
</body>
</html>
JavaScript — registration flow
const {
initSdk,
webauthnCreateCred
} = window.com$thalesgroup$gemalto$fido2$web;
window.addEventListener('load', async () => {
await initSdk();
});
document.getElementById('registerBtn').addEventListener('click', async () => {
const userId = document.getElementById('regUserId').value.trim();
const statusDiv = document.getElementById('regStatus');
try {
statusDiv.textContent = 'Starting registration...';
// Step 1: Get attestation options from your Relying Party backend
const optionsResponse = await fetch('/api/rp/fido2/attestation/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId })
});
const attestationOptions = await optionsResponse.json();
// Step 2: Create credential using the SDK
statusDiv.textContent = 'Please interact with your authenticator...';
const credential = await webauthnCreateCred({ options: attestationOptions });
// Step 3: Send credential to your Relying Party backend
const resultResponse = await fetch('/api/rp/fido2/attestation/result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential)
});
await resultResponse.json();
statusDiv.textContent = 'Registration successful';
} catch (error) {
console.error('Registration failed:', error);
statusDiv.textContent = 'Registration failed: ' + error.message;
if (error.name === 'NotAllowedError') {
statusDiv.textContent = 'Registration cancelled or timed out';
} else if (error.name === 'InvalidStateError') {
statusDiv.textContent = 'This authenticator is already registered';
}
}
});
JavaScript — authentication flow
const { webauthnGetCred } = window.com$thalesgroup$gemalto$fido2$web;
document.getElementById('authenticateBtn').addEventListener('click', async () => {
const userId = document.getElementById('authUserId').value.trim();
const statusDiv = document.getElementById('authStatus');
try {
statusDiv.textContent = 'Starting authentication...';
// Step 1: Get assertion options from your Relying Party backend
const requestBody = userId ? { userId } : {};
const optionsResponse = await fetch('/api/rp/fido2/assertion/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const assertionOptions = await optionsResponse.json();
// Step 2: Get credential using the SDK
statusDiv.textContent = 'Please interact with your authenticator...';
const credential = await webauthnGetCred({ options: assertionOptions });
// Step 3: Send credential to your Relying Party backend
const resultResponse = await fetch('/api/rp/fido2/assertion/result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential)
});
await resultResponse.json();
statusDiv.textContent = 'Authentication successful';
} catch (error) {
console.error('Authentication failed:', error);
statusDiv.textContent = 'Authentication failed: ' + error.message;
}
});
Step 3: Device Bound Key integration
The Device Bound Key (DBK) extension associates a hardware-backed cryptographic key with a FIDO credential. Follow these additional steps when using DBK.
Get the device-bound key identifier
Before initiating registration, call getDeviceBoundKeyId() to retrieve the dbkKid and check if the user already has a DBK on this browser:
const { getDeviceBoundKeyId } = window.com$thalesgroup$gemalto$fido2$web;
function base64urlEncode(str) {
const bytes = new TextEncoder().encode(str);
const base64 = btoa(String.fromCharCode(...bytes));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
async function getDbkInfoForUser(userId) {
const userIdB64Url = base64urlEncode(userId);
const { dbkKid, dbkExist } = await getDeviceBoundKeyId(userIdB64Url);
if (dbkExist) {
// User already has a DBK on this browser — skip registration or rebind instead
console.log('User already has a DBK on this browser:', dbkKid);
}
return { dbkKid, dbkExist };
}
Send the DBK key identifier to the backend
Include the dbkKid in the attestation options request so that the backend can pass it to the FIDO2 Server:
async function registerWithDbk(userId) {
const { dbkKid, dbkExist } = await getDbkInfoForUser(userId);
const optionsResponse = await fetch('/api/rp/fido2/attestation/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, dbkkid: dbkKid })
});
const attestationOptions = await optionsResponse.json();
const credential = await webauthnCreateCred({ options: attestationOptions });
await fetch('/api/rp/fido2/attestation/result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential)
});
}
Step 4: Multi-DBK Auto-Binding flow
The auto-binding flow enables users who already have a synced passkey to automatically bind it to a new browser's Device Bound Key, or to create a fresh credential if no existing passkey is found. It uses registerDeviceBoundCredential() with both assertion and attestation options from the server.
Frontend — auto-binding registration
const {
initSdk,
getDeviceBoundKeyId,
registerDeviceBoundCredential
} = window.com$thalesgroup$gemalto$fido2$web;
async function handleAutoBindingLogin(username) {
// 1. Get the base64url-encoded userId
const userId = await fetchUserId(username);
// 2. Check if user has a DBK on this browser
const { dbkKid, dbkExist } = await getDeviceBoundKeyId(userId);
if (dbkExist) {
return performFidoAuthentication(userId); // Normal FIDO authentication
}
// No DBK on this browser — verify via 2FA first
const tfaCode = await promptUserFor2FA();
await fetch('/api/rp/auth/2fa/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, tfaCode })
});
// 3. Get dual options from backend
const dualOptionsResp = await fetch('/api/rp/fido2/dual-options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, dbkKid })
});
const { assertionReq, attestationReq } = await dualOptionsResp.json();
// 4. Register device-bound credential
const result = await registerDeviceBoundCredential({
assertionReq,
attestationReq,
onFallbackConfirmation: () => new Promise((resolve) => {
showConfirmModal(
'No passkey found on this browser. Register a new credential?',
{ onConfirm: () => resolve(true), onCancel: () => resolve(false) }
);
})
});
// 5. Submit result to backend
const endpoint = result.type === 'assertion'
? '/api/rp/fido2/assertion/result'
: '/api/rp/fido2/attestation/result';
await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result.credential)
});
}
Backend — dual options endpoint
// POST /api/rp/fido2/dual-options
app.post('/api/rp/fido2/dual-options', async (req, res) => {
const { userId, dbkKid } = req.body;
if (!req.session.tfaVerified) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
const assertionReq = await callFido2Server('/assertion/options', {
userId,
extensions: {
thalesgroup_dbk_ext: {
v: 1,
kid: dbkKid,
allowNewDbkBinding: true,
allowedAlgorithms: [-7, -35, -36]
}
}
});
const attestationReq = await callFido2Server('/attestation/options', {
userId,
extensions: {
thalesgroup_dbk_ext: {
v: 1,
kid: dbkKid,
allowedAlgorithms: [-7, -35, -36]
}
}
});
res.json({ assertionReq, attestationReq });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Error handling for auto-binding
try {
const result = await registerDeviceBoundCredential({ assertionReq, attestationReq, onFallbackConfirmation });
} catch (error) {
if (error.code === 'WEB-0016') {
// User declined — continue session using 2FA authentication only
showMessage('Passkey registration skipped. You can still log in using 2FA.');
} else if (error.code === 'WEB-0015') {
console.error('Missing assertionReq or attestationReq.');
} else if (error.code === 'WEB-0012') {
console.error('allowNewDbkBinding not set to true in assertionReq.');
} else {
console.error(`Error [${error.code}]:`, error.details ?? error.message);
}
}
Testing your integration
Test registration
- Open your web application in a browser over HTTPS (or
localhost). - Click the Register button and enter a user ID.
- Interact with your authenticator when prompted.
- Verify the credential is created and the backend returns a success response.
Test authentication
- Click the Authenticate button.
- Optionally enter the same user ID used during registration.
- Interact with your authenticator when prompted.
- Verify the authentication succeeds.
Test error scenarios
- Cancel the authenticator prompt — should produce a
NotAllowedError. - Register the same authenticator twice — should produce an
InvalidStateError. - Authenticate without registering — should produce a
NotFoundErrororDBK-0015.
Related documentation
- API reference: Full function documentation
- Error handling: Error codes and handling patterns
- Device Bound Key extension: Server-side DBK configuration
- FIDO authentication APIs: FIDO2 Server REST API reference