@opensyber/tokenforge
v1.1.0
Published
Device-bound session security — W3C DBSC + ECDSA P-256 signatures + AitM detection. Drop-in for Auth0 / Okta / Clerk / Entra ID.
Maintainers
Readme
@opensyber/tokenforge
TokenForge Cloud v2 integration kit
The 1.1.0 Cloud path keeps the TokenForge bearer credential on the
application server. The browser creates a non-extractable P-256 key, binds only
through an authenticated application endpoint, and signs the exact v2 request
contract.
import { TokenForgeDevice } from '@opensyber/tokenforge/client';
const device = await TokenForgeDevice.open({
bindEndpoint: '/api/v1/auth/device/bind',
fetch: authenticatedFetch,
});
await device.bindAuthenticatedSession();
export const secureFetch = device.wrapFetch(authenticatedFetch);The server adapter is responsible for session resolution, the server-only
credential, local/cloud decision composition, circuit breaking, and telemetry.
Set TOKENFORGE_CLOUD_MODE to off, monitor, or enforce; default/unknown
values are off. In monitor, a locally valid request stays available while
cloud failures and would-block outcomes are reported. In enforce, definitive
cloud rejection or cloud unavailability rejects the request.
See @opensyber/[email protected] for the normative binary
contract, schemas, migration guide, and cross-language vectors.
Cloud v2 server boundary
Cloud v2 intentionally separates application authentication from TokenForge
device proof. The application owns the authenticated bind route and resolves
the current session on the server. Only that route may add a
TokenForgeIdentity envelope before delegating to the generic bind handler.
The browser never supplies trusted profile or subject fields.
import {
createTokenForgeCloudBindHandler,
createTokenForgeCloudMiddleware,
} from '@opensyber/tokenforge/server';
import type { TokenForgeIdentity } from '@opensyber/tokenforge/identity';
// Configure these once in server-only application code. The generated types
// describe the required session resolver, replay store, cloud client, mode,
// telemetry, and circuit-breaker dependencies.
export const bindDevice = createTokenForgeCloudBindHandler(bindOptions);
export const protectRequest = createTokenForgeCloudMiddleware(verifyOptions);
// Your authenticated route resolves identity from its own session or IdP,
// then passes the standard Request to bindDevice. Never forward identity data
// accepted from the browser.
const identity: TokenForgeIdentity = await resolveIdentityFromSession(request);Framework adapters expose the same verification contract without duplicating cryptography or authorization logic:
| Framework | Cloud v2 entry point | Request bridge |
|---|---|---|
| Next.js | withTokenForgeCloudV2 / tokenForgeCloudV2Check | Native Request |
| Hono | tokenForgeCloudV2Middleware | context.req.raw |
| SvelteKit | tokenForgeCloudV2Handle | event.request |
| Astro | tokenForgeCloudV2Middleware | context.request |
| Express | tokenForgeCloudV2Middleware | Explicit toRequest required |
| Fastify | tokenForgeCloudV2Plugin | Explicit toRequest required |
Express and Fastify require an application-owned toRequest converter because
the SDK must not guess the external origin, proxy trust, or request-body
semantics. Conversion failure is rejected rather than silently bypassing
TokenForge.
The same named bind adapter is available from every framework entry point:
import { createTokenForgeCloudBindHandler } from '@opensyber/tokenforge/server';
import { tokenForgeCloudV2BindRoute } from '@opensyber/tokenforge/nextjs';
const bindDevice = createTokenForgeCloudBindHandler(bindOptions);
export const POST = tokenForgeCloudV2BindRoute(bindDevice);For Hono, SvelteKit, and Astro, pass the same generic handler to
tokenForgeCloudV2BindRoute. Express and Fastify take a second argument with
the same explicit toRequest converter used by their verification adapter:
app.post(
'/api/v1/auth/device/bind',
tokenForgeCloudV2BindRoute(bindDevice, { toRequest }),
);Mount this route only after the application's authentication middleware. The generic bind handler must resolve the session and identity from trusted server-side state; never copy subject or profile fields from the request body.
Recommended adoption order:
- Mount the authenticated bind route and verify identity correlation in
offmode. - Enable
monitorand inspect local denials, cloud would-block decisions, outages, and replay telemetry without changing application availability. - Resolve all unexplained monitor findings and switch selected protected paths
to
enforce. - Keep public health, callback, and webhook paths in the exact
skipPathsallowlist; do not use substring matching.
The older tokenForgeMiddleware, tokenForgePlugin, withTokenForge, and
tokenForgeHandle examples below use the legacy v1 fail-open cloud behavior.
They remain exported for staged migration but should not be selected for a new
Cloud v2 integration.
Device-bound session security for the post-AiTM era. W3C DBSC–aligned, ECDSA P-256 + WebAuthn, drop-in for Auth0 / Okta / Clerk / Microsoft Entra ID. Every request after login is cryptographically signed with a device key that never leaves the browser. A stolen cookie without the device key is useless.
Why this matters in 2026
- Session hijacking attacks grew 127% YoY (Microsoft, May 2026)
- Adversary-in-the-Middle (AiTM) toolkits — EvilProxy, Tycoon — bypass MFA in real time
- Chrome 146 (April 2026) shipped browser-native DBSC for Windows; macOS/Linux pending
- Auth0 and Okta's "session protection" features rely on IP/ASN/UA fingerprinting — defeated by VPN-equipped attackers. TokenForge uses cryptographic device binding — defeats them.
Quick Start
1. Initialize after authenticated login
import { TokenForgeDevice } from '@opensyber/tokenforge/client';
const device = await TokenForgeDevice.open({
bindEndpoint: '/api/v1/auth/device/bind',
fetch: authenticatedFetch,
});
await device.bindAuthenticatedSession();
const secureFetch = device.wrapFetch(authenticatedFetch);The browser receives no TokenForge API key. It generates a non-extractable ECDSA P-256 device key, sends only the public JWK through the authenticated application bind endpoint, and signs the exact request-bound v2 contract.
2. Add server middleware
npm install @opensyber/tokenforge// Legacy v1 Express migration path
import { tokenForgeMiddleware } from '@opensyber/tokenforge/express';
app.use(tokenForgeMiddleware({ apiKey: process.env.TOKENFORGE_API_KEY! }));
// req.tf.bound, req.tf.trustScore, req.tf.deviceId
// Legacy v1 Next.js migration path
import { withTokenForge } from '@opensyber/tokenforge/nextjs';
export const GET = withTokenForge(handler, { apiKey: process.env.TOKENFORGE_API_KEY! });
// Legacy v1 Fastify migration path
import { tokenForgePlugin } from '@opensyber/tokenforge/fastify';
fastify.register(tokenForgePlugin, { apiKey: process.env.TOKENFORGE_API_KEY! });
// Legacy v1 Hono migration path
import { tokenForgeMiddleware } from '@opensyber/tokenforge/hono';
app.use('/api/*', tokenForgeMiddleware({ apiKey: env.TOKENFORGE_API_KEY }));3. Get your API key
Sign up at tokenforge.opensyber.cloud — free tier: 1,000 verifications/month, no credit card.
What's new in v1.0.0 (May 2026)
This release graduates the protocol surface from beta. Subsequent 1.x is backward-compatible additions only.
W3C DBSC protocol (Sprint 37)
Aligned with the W3C draft + Chrome 146 native rollout:
POST /v1/dbsc/challenge — issue one-shot challenge (register/refresh/step_up)
POST /v1/dbsc/register — bind device with JWS-signed challenge response
POST /v1/dbsc/refresh — rotate bound cookie, run risk policy
POST /v1/dbsc/sessions/:id/revoke — admin soft-revoke
GET /.well-known/tokenforge/jwks — public verifier keys (5-min edge cache)
GET /.well-known/tokenforge/dbsc — service descriptor for SDK auto-discoveryWorkforce SSO replaces Cisco Duo Premier (Sprint 36)
Five OIDC IdPs + SAML 2.0:
import { exchangeSso } from '@opensyber/tokenforge/server/internal';
// After Okta / Entra / Google Workspace / Auth0 / generic OIDC login
const result = await exchangeSso(db, store, {
tenantId, workforceAppId,
idToken: req.body.idToken, // from your IdP
jwks, // cached via getJwks()
});
// result: { ok: true, subjectId, externalSubject, email, challenge, challengeExpiresAt }JWKS cache: 24-hour freshness with stale-fallback when IdP is unreachable. xmlsoap claim namespace for Microsoft AD FS / Azure AD compatibility built in.
AitM detection + per-route step-up (Sprint 39)
import { requireFreshSig } from '@opensyber/tokenforge/server';
app.use('/admin/*', requireFreshSig({ minTrustScore: 90 }));
app.use('/billing/*', requireFreshSig({
minTrustScore: 95,
requireWebAuthn: true,
}));Per-tenant policy via tf_tenants.step_up_actions JSON:
[
{ "path": "/admin/billing", "requireFreshSig": true, "freshSigMaxAgeSec": 30 },
{ "path": "/admin/*", "requireFreshSig": true, "requireWebAuthn": true }
]Exact match wins over glob. Glob /admin/* matches /admin/users but not /admin (no segment past prefix).
Action signing for sensitive operations
// Client
const sig = await tokenforge.signAction({
action: 'transfer',
body: { fromAccount, toAccount, amount },
});
fetch('/api/transfer', {
method: 'POST',
headers: { 'X-TF-Action-Signature': sig },
body: JSON.stringify({ fromAccount, toAccount, amount }),
});5-second freshness window. JWS claims include actionHash (SHA-256 over canonicalized body) so an attacker can't replay the signature with a different transfer amount.
Webhook event stream
12 events, HMAC-SHA256 signed, retried with [1s, 4s, 15s] backoff, stable X-TF-Delivery-Id across retries:
session.bound session.verified session.revoked
trust_score.degraded trust_score.critical session.hijack_attempt
usage.cap_exceeded dbsc.risk_signal dbsc.policy_block
dbsc.session_step_up dbsc.session_revoked webhook.testimport { verifyWebhookSignature } from '@opensyber/tokenforge/webhooks';
app.post('/webhooks/tokenforge', async (c) => {
const rawBody = await c.req.text();
const ok = await verifyWebhookSignature({
body: rawBody,
signatureHeader: c.req.header('X-TF-Signature') ?? '',
timestampHeader: c.req.header('X-TF-Timestamp') ?? '',
secret: c.env.TOKENFORGE_WEBHOOK_SECRET,
});
if (!ok) return c.json({ error: 'bad_signature' }, 401);
// handle event ...
});Secret rotation grace window: 24 hours. Send the new secret while the old one stays valid; receivers verify against either.
How it works
Browser TokenForge API Your Server
│ │ │
│ 1. Generate ECDSA P-256 │ │
│ keypair (non-extractable) │ │
│ │ │
│ 2. POST /v1/dbsc/challenge ───>│ │
│ <─────── { challenge } ────────│ │
│ │ │
│ 3. POST /v1/dbsc/register ────>│ Store public key │
│ + JWS over challenge │ │
│ <─── { sessionId, deviceId } ──│ │
│ │ │
│ 4. fetch('/api/data') │ │
│ + X-TF-Signature ──────────────────────────────────────> │
│ │ │
│ │ <── POST /v1/edge/verify ── │
│ │ Verify signature │
│ │ Run AitM heuristics │
│ │ Compute trust score │
│ │ ──> { allow, score: 92 } │
│ <──────────────────────────────────── 200 OK ─────────────── │The device key never leaves the client. The TokenForge service holds the public key + session metadata. Your server passes request context, gets back an allow/step_up/block decision.
Trust score signals
| Signal | Weight | Detects | |--------|--------|---------| | Signature | 30 | Tampering, missing/wrong device key | | IP Address | 15 | IP change since binding | | Geo Location | 15 | Country mismatch | | Fingerprint | 15 | Browser/device fingerprint drift | | Velocity | 10 | Multiple IPs in short window | | Timing | 10 | Clock skew beyond ±60s | | Nonce | 5 | Replay attacks |
Score >= 80: allow. Score 40-79: step_up. Score < 40: block. Thresholds configurable per tenant.
Drop-in for your existing IdP
TokenForge runs after authentication — bring your own IdP:
| IdP | Integration | Built-in support |
|---|---|---|
| Microsoft Entra ID | Custom Authentication Extension webhook | Roadmap M11 (Q3 2026) |
| Auth0 | Auth0 Action snippet | Roadmap M14 — Marketplace listing pending review |
| Okta | Inline Hook + Custom Authenticator | Roadmap M15 — OIN listing pending review |
| Clerk | @tokenforge/clerk-middleware shim | Roadmap M16 |
| Auth.js / NextAuth | Works today via @opensyber/tokenforge/nextjs | ✅ |
| Firebase Auth | Works today via @opensyber/tokenforge/express | ✅ |
| Supabase Auth | Works today via @opensyber/tokenforge/express | ✅ |
| Custom JWT | Works today | ✅ |
React integration
import { TokenForgeProvider, useTokenForge } from '@opensyber/tokenforge/react';
function App() {
return (
<TokenForgeProvider config={{
apiBase: '/api',
getSessionId: () => getSession(),
}}>
<YourApp />
</TokenForgeProvider>
);
}
function ProtectedPage() {
const { bound, trustScore, deviceId } = useTokenForge();
if (!bound) return <BindButton />;
return <Dashboard trustScore={trustScore} deviceId={deviceId} />;
}Self-hosted server
The server adapters work against your own database / KV — no hosted service required:
import { createTokenForgeRoutes } from '@opensyber/tokenforge/server';
import { D1Storage } from '@opensyber/tokenforge/server/storage';
// or PostgresStorage, RedisStorage, or implement the StorageInterface
const tf = createTokenForgeRoutes({
storage: new D1Storage(env.DB),
sessionMaxAge: 86400,
});
app.route('/api/tf', tf);Storage backends shipped: D1 (Cloudflare Workers), PostgreSQL, Redis. Bring your own via StorageInterface.
Compatibility
| Platform | Status | |---|---| | Chrome 146+ Windows | ✅ Native DBSC + polyfill fallback | | Chrome 146+ macOS / Linux | ⏳ DBSC pending Google rollout; polyfill works today | | Safari (macOS / iOS) | ✅ Polyfill via Web Crypto + IndexedDB | | Firefox | ✅ Polyfill | | Edge 146+ | ✅ Inherits Chromium DBSC | | Node.js 18+ (server) | ✅ | | Bun, Deno | ✅ | | Cloudflare Workers | ✅ |
Pricing (hosted service)
| Plan | Price | Verifications/mo | |------|-------|-----------------| | Free | $0 | 1,000 | | Pro | $49/mo | 50,000 | | Team | $199/mo | 250,000 | | Enterprise | Custom | Unlimited + SLA |
The SDK code in this repository is MIT-licensed and runs against any storage backend — you can self-host without a TokenForge subscription.
Examples & docs
- Full API reference: tokenforge.opensyber.cloud/docs
- DBSC protocol explainer: tokenforge.opensyber.cloud/dbsc
- Reference apps (Next.js, Express, Hono, Fastify): github.com/opensyber/tokenforge-examples
Security disclosure
Security issues: [email protected] (PGP key available). Coordinated disclosure within 90 days; CVE assignment for confirmed vulnerabilities.
License
SDK: MIT (see LICENSE.md). Hosted service requires API key — governed by TokenForge Terms.
