Overview
The Grantex-hosted service enabled portable WebAuthn evidence on September 27, 2026. Production browser E2E verified enrollment, live approval, assertion evidence, refresh, delegation, account response policy, and public VC revocation status. Self-hosted deployments still default to off and must follow the rollout below. Grantex supports FIDO2/WebAuthn passkeys during consent. A valid assertion proves use of a credential bound to the principal and to Grantex’s challenge. When hosted enrollment is enabled, the server also requires WebAuthn user verification during registration and assertion; this may be a device PIN, biometric, or other authenticator-supported method. The server does not learn which method the customer used. Live-mode consent requires an existing passkey for the principal. Sandbox-mode accounts can also require one withfidoRequired: true. A consent link alone cannot register a passkey because it does not authenticate the principal.
The Grantex-hosted service runs with PASSKEY_ENROLLMENT_ENABLED=true, FIDO_RP_ID=grantex.dev, and FIDO_ORIGIN=https://grantex.dev; its production browser flow has passed enrollment and live-consent E2E. Self-hosted deployments must enable the flag and configure their own public HTTPS origin and RP ID. The feature is off by default there; do not enable live mode for customers until they can complete an enrollment test on that origin.
The hosted enrollment client is published in @grantex/sdk@0.8.0, grantex==0.7.0, and github.com/mishrasanjeev/grantex-go@v0.4.1. These are independently versioned packages; installation does not turn on the server feature. Check Release Status for current registry evidence and rollout limitations.
Portable evidence is a separate, default-off source rollout: PORTABLE_WEBAUTHN_EVIDENCE_ENABLED=true. It requires IRREGULARITY_CASCADE_REVOCATION_ENABLED=true so automatic grant revocation also updates VCs and public status lists, and PORTABLE_WEBAUTHN_EVIDENCE_STATUS_CHECK_ENABLED=true so issuer verification checks stored grant and ancestor status. Keep both safety flags enabled if issuance is rolled back. The hosted service has all three enabled. TypeScript, Python, and Go SDKs expose the typed grant evidence reference and VC attestation in their newly verified releases; custom assertion UIs still use the REST endpoints directly.
Why FIDO for Agents?
Standard consent flows rely on a button click in a browser. A verified WebAuthn assertion establishes that:- The specific device registered by the user is present
- User verification was performed when the hosted-enrollment policy requires it; the legacy developer registration path only requests it as preferred
- The assertion is bound to the specific challenge issued by Grantex
Developer Setup
Sandbox And Live Parity
For a realistic sandbox rehearsal, set
fidoRequired: true before requesting
authorization. Both /v1/authorize and OAuth PAR then redirect to consent
instead of returning an automatic code. Developer-key approve/deny shortcuts
cannot bypass that requirement. Live mode never inherits sandbox auto-approval.
OAuth requests without a principal hint let the customer enter the principal
identifier, then verify a passkey registered for that principal; typing an
identifier is not itself authentication.
Enable FIDO for your account using the SDK or REST API:
boolean
default:"false"
When
true, sandbox-mode authorization requests also require a WebAuthn assertion. Live mode already requires one regardless of this setting.string
The Relying Party name displayed in the browser’s passkey prompt. Typically your application name.
Hosted Enrollment For End Customers
Your application must first authenticate the end customer and bind that session to the exactprincipalId it uses in Grantex. From your server, issue a short-lived, one-use enrollment link and deliver it to that customer through the authenticated session. Never expose your developer API key in browser code. The enrollment link itself is a bearer secret: do not log it, put it in analytics, or send it to another customer.
grantex==0.7.0 uses client.webauthn.create_enrollment_session(principal_id=principal_id, auth_request_id=auth_request_id); Go v0.4.1 uses client.WebAuthn.CreateEnrollmentSession(ctx, grantex.WebAuthnEnrollmentSessionParams{PrincipalID: principalID, AuthRequestID: authRequestID}). Both calls belong on your authenticated application server, not in browser code.
The customer opens the link at /passkey-enroll, uses their device’s passkey prompt, and returns to consent when the link was bound to an authorization request. The browser registration challenge expires after five minutes and must be restarted if it expires. The registration requires device user verification. An enrollment ticket can register only one credential and cannot be reused. To add another device, create another link.
The developer portal’s Passkeys page can also issue a link for a principal. Operators must establish the customer’s identity before sharing it. There is deliberately no unauthenticated “register” button on the consent page: anyone holding a consent URL could otherwise enroll their own passkey as the principal.
If enrollment is disabled (404) or the customer cannot use a passkey-capable browser, live-mode approval remains blocked. Sandbox mode can be used for integration testing, but it is not an alternative live-mode approval path.
For production rollout, deploy the auth service and the /passkey-enroll public-hosting rewrite first. Confirm FIDO_ORIGIN is the exact public HTTPS origin and FIDO_RP_ID is its valid relying-party domain, then enable PASSKEY_ENROLLMENT_ENABLED=true. Test registration and live approval on that origin before sending links to customers. The repository includes apps/auth-service/tests/production/passkey-policy.test.ts and the manual Production Passkey and Irregularity E2E GitHub Actions workflow. That test creates an isolated production account, passkey, grant and audit records; it must not use a customer account.
Portable Evidence Rollout
To enable portable evidence, first deploy migration119_webauthn_evidence.sql and confirm the enabled Docker/Chromium E2E suite and all CI checks are green. Test the deployed RP ID/origin with an isolated account. Enable IRREGULARITY_CASCADE_REVOCATION_ENABLED=true and PORTABLE_WEBAUTHN_EVIDENCE_STATUS_CHECK_ENABLED=true, then reconcile historical revoked grants and credentials using node dist/scripts/reconcile-revoked-vcs.js (dry run) followed by --apply on an authorized maintenance worker. Only then enable PORTABLE_WEBAUTHN_EVIDENCE_ENABLED=true on the auth service; startup rejects the portable flag without both safety flags. Run the Production Passkey and Irregularity E2E workflow after activation. New assertions will carry evidence; existing grants do not gain it retroactively. Approved live requests that predate activation have no captured assertion and fail token exchange with PASSKEY_EVIDENCE_REQUIRED; restart consent for those customers. To stop new issuance, disable only PORTABLE_WEBAUTHN_EVIDENCE_ENABLED; leave the two safety flags on for already-issued credentials.
Before trusting old VCs’ public status lists, reconcile active child grants and credentials left behind by historical grant-only irregularity revocations. From an auth-service build with database access, run node dist/scripts/reconcile-revoked-vcs.js for counts, then node dist/scripts/reconcile-revoked-vcs.js --apply to cascade the old revocations and update VC rows and status-list bits. This command is idempotent and prints counts, not credential data. Keep cascade revocation and evidence status checks enabled even if portable issuance is rolled back: turning off only PORTABLE_WEBAUTHN_EVIDENCE_ENABLED stops new capture and restores legacy exchange for old no-evidence requests, while previously captured evidence remains verifiable and propagates on refresh/delegation. Keep the opt-in VCs’ stable public keys only as long as necessary.
For local verification, start a disposable Postgres instance, set AUDIT_INTEGRATION_DATABASE_URL, install Chromium with npx playwright install chromium, and run npm run test:e2e from apps/auth-service. The browser suite uses a virtual authenticator and a real local Postgres database; it cannot by itself prove that a hosted production origin, routing rewrite, or customer device is configured correctly.
Developer-Driven Registration Ceremony
The hosted link above is the recommended customer flow. The older developer-authenticatedregister/options and register/verify endpoints can drive registration from your own application, but they request user verification as preferred, not required. Do not treat a credential created through that path as verified human presence at enrollment. Register a new passkey for each device.
Step 1: Get Registration Options
Step 2: Create Credential in Browser
Pass the options to the browser’s WebAuthn API:Step 3: Verify and Store
Send the credential response back to your server and forward it to Grantex:Assertion Ceremony (Consent Flow)
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.Programmatic Assertion
If you build a custom consent UI, callPOST /v1/webauthn/assert/options with the pending authRequestId and bound principalId. The response contains { challengeId, publicKey }; decode publicKey.challenge and each publicKey.allowCredentials[].id from base64url before calling navigator.credentials.get({ publicKey }). Serialize the returned assertion’s id, rawId, type, response.clientDataJSON, response.authenticatorData, response.signature, optional response.userHandle, and clientExtensionResults as base64url where applicable. Send { challengeId, response } to POST /v1/webauthn/assert/verify, then separately call POST /v1/consent/{authRequestId}/approve or /deny. Verification only marks the pending request as FIDO-verified; it does not approve the grant by itself. The hosted consent page implements this sequence. The published TypeScript, Python, and Go WebAuthn clients do not currently expose assertOptions/assertVerify helper methods; use these REST endpoints for a custom UI.
Managing Credentials
Users can have multiple passkeys registered (e.g., laptop fingerprint + phone + YubiKey). Use the credentials endpoints to list and delete them. Enroll each additional device with a new one-use link. An authenticator that already has a credential for this principal is excluded from duplicate registration. Deleting a credential stops future assertions with that key; it does not retroactively invalidate existing grants or historical assertion evidence. Revoke the affected grants separately when removing access. Agents with issued VCs retain their grant and credential history: attempting to hard-delete one returns409 AGENT_HAS_CREDENTIAL_HISTORY. Suspend the
agent and revoke its grants instead; its existing public credential status
must remain available.
Portable Assertion Evidence
With portable evidence enabled, successful assertion verification stores the raw assertion, challenge, credential public key, prior counter, RP ID, origin, and assertion time on the pending authorization request. Approval or denial remains a separate operation. Token exchange rechecks the assertion, binds it to the grant, and places a compactwebauthnEvidence digest reference in the signed grant token and grant API response. Refresh preserves this reference. An approved live request from before evidence capture cannot be exchanged without new passkey consent while the flag is enabled.
Request credentialFormat: "vc-jwt" or "both" at token exchange to obtain a VC with the complete GrantexWebAuthnAssertion in vc.evidence. The public credential key in this opt-in export is stable and can correlate presentations; minimize sharing and retention. Independent verifiers must check the VC’s issuer signature, expiry and revocation, pin a trusted RP ID and origin, reverify the assertion signature/challenge/counter, and compare its digest with the grant reference. The authenticator did not sign the scopes or independently prove the customer’s legal identity: the issuer signature and your enrollment identity checks supply those bindings. See Verifiable Credentials. No payment-network certification is claimed.
API Reference
Security Considerations
- User verification: Hosted enrollment requires
userVerification: "required"and verifies it on the server. With enrollment enabled, consent assertions also require it. The legacy developer-authenticated registration API requests"preferred"; do not treat those older ceremonies as proof of user verification. - 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. - Missing credential: Consent cannot be approved until the application issues an authenticated enrollment link and the customer registers a passkey. There is no weaker live-mode fallback.
Release Validation Boundary
The repeatable browser checks are intests/e2e/passkey-policy-browser.e2e.test.ts (real Postgres and Chromium),
tests/production/passkey-parity.test.ts (sandbox/live parity, multiple devices,
denial, approval, principal selection, OAuth callbacks and revocation), and
tests/production/passkey-policy.test.ts (portable evidence, refresh,
delegation, public VC status and both irregularity response modes), and
tests/production/passkey-portal.test.ts (dashboard login, enrollment-link
issuance, credential listing and confirmed removal).
The manual Production Passkey and Irregularity E2E workflow runs all three
production files against isolated accounts on the hosted HTTPS origin.
Credential removal sends an authenticated, bodyless DELETE request. Do not
attach Content-Type: application/json without a JSON body; the API rejects
that malformed request with HTTP 400. The dashboard handles the successful
HTTP 204 response without attempting to parse an empty JSON response.
Successful automation is sign-off for these tested service/browser contracts,
not a guarantee that every customer device, operating system or browser works.
Chromium uses virtual authenticators. Before rollout to your customers, test
your supported physical devices, user-verification prompts, cancellation,
recovery and accessibility. Your application still owns customer
authentication, principal binding and secure delivery of enrollment links.
No unauthenticated enrollment, weaker live consent fallback, or
payment-network certification is supplied by these tests.
Next Steps
- Verifiable Credentials — assertion export and independent verification
- DID Infrastructure — how verifiers resolve Grantex’s signing keys
- End-User Permissions — user-facing permission dashboard