Skip to main content

Overview

Grantex supports FIDO2/WebAuthn passkeys as a human presence verification mechanism during the consent flow. When enabled, end-users authenticate with a biometric, security key, or platform authenticator before a grant is issued. This raises the assurance level from “user clicked approve” to “user was cryptographically verified by their device.” FIDO is opt-in per developer. When fidoRequired is set to true, all authorization requests for that developer require a WebAuthn assertion before the grant is approved.

Why FIDO for Agents?

Standard consent flows rely on a button click in a browser. This is vulnerable to session hijacking, UI redressing, and automated approval. A FIDO assertion proves that:
  1. The specific device registered by the user is present
  2. The user completed a biometric or physical authentication
  3. The assertion is bound to the specific challenge issued by Grantex
For high-value agent operations (payments, data access, contract signing), this level of assurance is required by emerging standards like the Mastercard Verifiable Intent specification.

Developer Setup

Enable FIDO for your account using the SDK or REST API:
import { Grantex } from '@grantex/sdk';

const grantex = new Grantex({ apiKey: process.env.GRANTEX_API_KEY });

await grantex.updateSettings({
  fidoRequired: true,
  fidoRpName: 'My Application',  // displayed in browser passkey prompts
});
fidoRequired
boolean
default:"false"
When true, all authorization requests for this developer require a WebAuthn assertion before the grant is approved.
fidoRpName
string
The Relying Party name displayed in the browser’s passkey prompt. Typically your application name.

Registration Ceremony

Before a user can authenticate with FIDO, they must register a passkey. This is a one-time process per user per device.
  Browser                          Your Server                     Grantex
    │                                  │                              │
    │                                  │─── registerOptions() ───────►│
    │                                  │◄── challenge + options ──────│
    │◄── publicKey options ────────────│                              │
    │                                  │                              │
    │── navigator.credentials.create() │                              │
    │── (user touches fingerprint) ────│                              │
    │                                  │                              │
    │── attestation response ─────────►│                              │
    │                                  │─── registerVerify() ────────►│
    │                                  │◄── credential stored ────────│
    │◄── success ──────────────────────│                              │

Step 1: Get Registration Options

const options = await grantex.webauthn.registerOptions({
  principalId: 'user_abc123',
});

// options contains:
// - challengeId: string (server-side reference)
// - rp: { name, id }
// - user: { id, name, displayName }
// - pubKeyCredParams: [{ type: 'public-key', alg: -7 }, ...]
// - authenticatorSelection: { userVerification: 'required' }
// - timeout: 60000
// - challenge: base64url-encoded

Step 2: Create Credential in Browser

Pass the options to the browser’s WebAuthn API:
// In the browser
const credential = await navigator.credentials.create({
  publicKey: {
    challenge: base64urlToBuffer(options.challenge),
    rp: options.rp,
    user: {
      id: base64urlToBuffer(options.user.id),
      name: options.user.name,
      displayName: options.user.displayName,
    },
    pubKeyCredParams: options.pubKeyCredParams,
    authenticatorSelection: options.authenticatorSelection,
    timeout: options.timeout,
  },
});

Step 3: Verify and Store

Send the credential response back to your server and forward it to Grantex:
await grantex.webauthn.registerVerify({
  challengeId: options.challengeId,
  response: {
    id: credential.id,
    rawId: bufferToBase64url(credential.rawId),
    type: credential.type,
    response: {
      clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
      attestationObject: bufferToBase64url(credential.response.attestationObject),
    },
  },
});
When FIDO is enabled, the consent flow includes a WebAuthn assertion challenge. This happens automatically in the hosted consent UI, but you can also drive it programmatically.
  Browser                          Grantex Consent UI              Grantex
    │                                  │                              │
    │── user opens consent URL ───────►│                              │
    │                                  │── assertOptions() ──────────►│
    │                                  │◄── challenge ────────────────│
    │◄── publicKey get options ────────│                              │
    │                                  │                              │
    │── navigator.credentials.get() ──►│                              │
    │── (user touches fingerprint) ────│                              │
    │                                  │                              │
    │── assertion response ───────────►│                              │
    │                                  │── assertVerify() ───────────►│
    │                                  │◄── grant approved ───────────│
    │◄── redirect to callback ─────────│                              │

Programmatic Assertion

If you build a custom consent UI, use the assertion endpoints directly:
// Get assertion options
const assertOpts = await grantex.webauthn.assertOptions({
  principalId: 'user_abc123',
  authRequestId: 'areq_01HXYZ...',
});

// Browser performs navigator.credentials.get()
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: base64urlToBuffer(assertOpts.challenge),
    allowCredentials: assertOpts.allowCredentials,
    userVerification: 'required',
    timeout: 60000,
  },
});

// Verify the assertion and approve the grant
await grantex.webauthn.assertVerify({
  challengeId: assertOpts.challengeId,
  response: {
    id: assertion.id,
    rawId: bufferToBase64url(assertion.rawId),
    type: assertion.type,
    response: {
      clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON),
      authenticatorData: bufferToBase64url(assertion.response.authenticatorData),
      signature: bufferToBase64url(assertion.response.signature),
    },
  },
});

Managing Credentials

Users can have multiple passkeys registered (e.g., laptop fingerprint + phone + YubiKey). Use the credentials endpoints to list and delete them.
// List all credentials for a user
const credentials = await grantex.webauthn.listCredentials('user_abc123');
for (const cred of credentials) {
  console.log(cred.id, cred.createdAt, cred.lastUsedAt);
}

// Delete a credential
await grantex.webauthn.deleteCredential('cred_01HXYZ...');

FIDO Evidence in Grants

When a grant is approved via FIDO assertion, the grant record includes FIDO evidence metadata:
{
  "grantId": "grnt_01HXYZ...",
  "fidoEvidence": {
    "credentialId": "cred_01HXYZ...",
    "authenticatorType": "platform",
    "userVerified": true,
    "assertedAt": "2026-03-08T12:00:00Z"
  }
}
This evidence is also embedded in Verifiable Credentials when credentialFormat: "vc-jwt" is used during token exchange. See Verifiable Credentials for details.

API Reference

MethodEndpointDescription
POST/v1/webauthn/register/optionsGenerate passkey registration options
POST/v1/webauthn/register/verifyVerify registration and store credential
GET/v1/webauthn/credentialsList WebAuthn credentials for a principal
DELETE/v1/webauthn/credentials/:idDelete a credential
POST/v1/webauthn/assert/optionsGenerate assertion options for consent
POST/v1/webauthn/assert/verifyVerify assertion during consent
PATCH/v1/meUpdate developer settings (FIDO config)

Security Considerations

  • User verification required: All FIDO operations set userVerification: "required", ensuring the authenticator performs biometric or PIN verification. "discouraged" or "preferred" are not accepted.
  • Challenge expiry: Registration and assertion challenges expire after 5 minutes. Replaying an expired challenge returns a 400 error.
  • Credential binding: Each credential is bound to a specific principal and developer. A credential registered for one developer’s consent flow cannot be used for another.
  • Attestation: Grantex accepts "none", "indirect", and "direct" attestation conveyance. Attestation statements are stored but not currently used for trust decisions.
  • Fallback behavior: If FIDO is enabled but the user has no registered credentials, the consent flow prompts them to register a passkey before proceeding.

Next Steps