Building passkey-based verification without trusting the browser.
Most applications need to answer a deceptively simple question: should this request be allowed to continue? Historically, many applications have relied on CAPTCHAs, SMS one-time passwords, or custom browser-side checks.
Most applications need to answer a deceptively simple question: should this request be allowed to continue?
Historically, many applications have relied on CAPTCHAs, SMS one-time passwords, or custom browser-side checks. Each approach comes with trade-offs involving user friction, delivery costs, phishing risk, or the difficulty of determining what a client can actually prove.
We have been building Chirograph Verify around a different approach: WebAuthn-based device verification, with the verification decision made on the server.
We're launching Chirograph Verify on October 30, 2026, and I wanted to share the engineering ideas behind it.
The browser should not be the authority
One principle shaped the implementation from the beginning: the browser should collect the authentication response, but it should not be trusted to decide whether verification succeeded.
A client can display a success message. That does not make the result trustworthy.
Chirograph Verify uses WebAuthn assertions generated through a user's passkey or supported security key. The server validates the submitted assertion and makes the verification decision.
This gives the application a cryptographic result to evaluate rather than relying solely on a client-side boolean.
What the verification flow looks like
At a high level, the flow has five stages:
An application initiates a verification request.
The server creates the appropriate challenge or hosted flow.
The user completes the WebAuthn ceremony on their device.
The server validates the response.
The application receives a verification result and, where applicable, a device-trust score.
The distinction between registration and authentication is important. A new user needs to enroll a passkey first; a returning user can authenticate using an enrolled credential.
Integrating a hosted widget
We built a hosted widget so developers do not have to implement every browser-side ceremony themselves.
The browser loads the widget bundle:
It then initializes the widget with a publishable key:
const widget = ChirographWidget.init({
baseUrl: "https://app.chirographverify.com",
widgetKey: "YOUR_PUBLISHABLE_WIDGET_KEY"
});
For a new user, the application starts a registration flow:
await widget.start({
userReference: "[email protected]",
purpose: "register",
redirectUrl: "https://your-app.example.com/auth/callback"
});
For an enrolled user, the purpose changes to authenticate.
The widget redirects the browser to the hosted ceremony and returns it to the configured callback URL afterward.
Why we separate browser keys from server secrets
A browser integration needs some configuration that can be public. It should never need the application's secret API credential.
Chirograph Verify therefore separates the publishable widget key from the tenant API key used by trusted servers.
The widget key starts a flow. The secret tenant API key is used for server-to-server operations, including redeeming a completed widget result.
That separation matters because code delivered to a browser is visible to the person running it. A secret placed in browser JavaScript is not a secret anymore.
A callback is not the verification decision
After the hosted ceremony, the browser receives flow_result and flow_id in the callback URL.
The application must not treat possession of those parameters as proof of successful verification.
Instead, its server sends the opaque result token to:
POST /v1/widget/redeem
The server authenticates the request with the tenant's secret X-API-Key and sends the result token in the request body.
The token is tenant-bound, expires, and can be redeemed only once. The application receives the authoritative verification outcome from the server-side exchange.
This is a small architectural distinction with an important security consequence: the browser transports the result reference, but the trusted server makes the decision.
What we learned building the integration
A few principles guided the implementation:
Validate origins and redirect destinations. A public widget key should not authorize a flow from an arbitrary website.
Separate credential scopes. Browser widget credentials and server API credentials have different responsibilities.
Treat results as short-lived capabilities. Result tokens should not become reusable authentication assertions.
Enforce tenant boundaries server-side. A client-supplied identifier should not override the tenant resolved from its authenticated credential.
Make the integration understandable. Developers need to know where registration ends, where authentication begins, and where server-side verification occurs.
These controls do not eliminate every form of abuse, and WebAuthn verification is not a universal replacement for application-specific authorization or fraud rules. It provides a cryptographic verification signal that applications can incorporate into their own decisions.
Try the developer integration
Chirograph Verify provides a hosted widget alongside a server-to-server verification API. The widget supports registration and authentication flows, while the API provides a server-verified result.
Start here:
Website: https://chirographverify.com/
Developer documentation: https://app.chirographverify.com/developers
Widget demonstration: https://app.chirographverify.com/demo
We're launching on October 30 and are looking forward to feedback from developers working on authentication, bot prevention, and application security.
The most useful feedback for us is technical: what is clear, what is missing from the integration experience, and which edge cases we should investigate next.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.