Session binding (DBSC)
Phished credentials are a solved problem — passkeys won’t sign a fake origin. But what about the session after the user logs in? A malware-stolen bearer token can impersonate the user for hours, from anywhere, and no authentication step will catch it.
TryMellon implements Device-Bound Session Credentials (DBSC, the W3C draft). On login, the browser generates a TPM-backed ECDSA P-256 keypair. Every session refresh requires a Proof-of-Possession (PoP) JWT signed with that key. The session token becomes useless the moment it leaves the device it was issued on.
Google enabled DBSC in Chrome 113+ as a countermeasure to cookie-theft malware. TryMellon makes it a first-class session primitive — no extension, no bespoke code in your app.
The attack DBSC blocks
Without DBSC With DBSC
───────────── ─────────
1. User logs in 1. User logs in + device registers TPM key
2. Bearer token set 2. Bearer token set + sec_session_id recorded
3. Malware exfiltrates cookie 3. Malware exfiltrates cookie
4. Attacker reuses cookie from 4. Attacker reuses cookie from attacker machine
attacker machine — ACCEPTED — attacker has no TPM private key
5. Full account takeover 5. Next refresh fails PoP check → session revoked
Multi-factor, device fingerprinting, and IP reputation all happen at auth time. DBSC happens at every request that refreshes the session. It turns a stolen token from a persistent breach into a seconds-long one.
When to use it
| Product shape | Why DBSC pays off |
|---|---|
| CeFi / crypto exchanges | Token theft is the dominant residual risk after passkeys. DBSC closes it. |
| Consumer fintech with web access | Malware targeting browser cookies is a $Bn industry. This is the specific fix. |
| Enterprise admin consoles | Session hijacking from developer machines is the top IR case in the enterprise segment. |
| High-assurance B2B | DBSC is a clear SOC2 / ISO differentiator vs Auth0/Okta defaults. |
If your users only reach your app via native mobile (no browser), DBSC buys less — native apps have their own keystore primitives. Most deployments have some browser traffic.
How it works
Browser Your backend TryMellon API
│ │ │
│── sign in (passkey / SDK) ────────────>│ │
│ │── validate session ─────────>│
│ │<── { session_token, sec_session_id }
│<── Sec-Session-Registration header ────│ │
│ │ │
│ generate ECDSA P-256 keypair │ │
│ private key lives in TPM / Secure │ │
│ Enclave — cannot be exported │ │
│ │ │
│── POST /v1/session-binding/register ──────────────────────────────────>│
│ { public_key_jwk, challenge_sig } │
│<── { sec_session_id, refresh_interval_ms } ────────────────────────────│
│ │
│ every refresh_interval_ms: │
│ sign a PoP JWT with the private key │
│ │
│── POST /v1/session-binding/verify ────────────────────────────────────>│
│ { sec_session_id, pop_jwt } │
│<── { ok: true } or { ok: false, revoke } │
- On login, the browser receives a
Sec-Session-Registrationchallenge. - The browser generates a TPM-backed ECDSA P-256 keypair. The private key is non-exportable.
- The browser posts the public JWK and a signature over the challenge to
/v1/session-binding/register. TryMellon stores the public key tied to the session. - At a configured interval (default 10 minutes), the browser generates a fresh PoP JWT signed with the device key and sends it to
/v1/session-binding/verify. - A verification failure marks the session for revocation — your SDK receives
session.revokedand forces re-authentication.
Quick start
DBSC is enabled per deployment via the DBSC_ENABLED flag. When enabled, the SDK handles the browser side transparently — you only need to allow the register/verify round-trips to reach TryMellon.
Server-to-server: register the session
POST https://api.trymellonauth.com/v1/session-binding/register
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/json
{
"public_key_jwk": {
"kty": "EC",
"crv": "P-256",
"x": "base64url-x-coordinate",
"y": "base64url-y-coordinate"
},
"algorithm": "ES256",
"challenge_signature": "base64url-signature-over-challenge",
"origin": "https://your-app.com"
}
Response — 201 Created:
{
"ok": true,
"data": {
"sec_session_id": "sec_01HXYZ…",
"expires_at": "2026-04-17T11:00:00.000Z",
"refresh_interval_ms": 600000
}
}
Server-to-server: verify a PoP JWT
POST https://api.trymellonauth.com/v1/session-binding/verify
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/json
{
"sec_session_id": "sec_01HXYZ…",
"pop_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0Iiwiandr…"
}
Response — success:
{
"ok": true,
"data": {
"verified": true,
"sec_session_id": "sec_…",
"next_refresh_in_ms": 600000
}
}
Response — revoked:
{
"ok": false,
"error": { "code": "dbsc_signature_invalid", "message": "PoP JWT signature verification failed" }
}
A failure here must be treated as a session compromise: revoke, force re-auth, and log.
API reference
POST /v1/session-binding/register
-
Auth: Basic Auth (S2S).
-
Request body:
Field Type Required Description public_key_jwkJWK object yes ECDSA P-256 public key. algorithm"ES256"yes Only ES256 is accepted today. challenge_signaturestring(base64url)yes Signature over the server-issued challenge. originstring(URL)yes Origin from which registration was initiated. Must match rpId. -
Response:
{ sec_session_id, expires_at, refresh_interval_ms }. -
TTL: controlled by the
DBSC_SESSION_TTL_SECONDSenv (default 3600).
POST /v1/session-binding/verify
-
Auth: Basic Auth (S2S).
-
Request body:
Field Type Required Description sec_session_idstring(UUID)yes Returned by /register.pop_jwtstringyes Fresh PoP JWT signed with the device key. -
Response:
{ verified, sec_session_id, next_refresh_in_ms }on success.
Browser support
DBSC requires a modern browser with TPM access:
| Browser | Status |
|---|---|
| Chrome 113+ (desktop) | ✅ Full support (origin trial → stable) |
| Edge 113+ | ✅ Via Chromium |
| Safari | ⚠️ Not implemented as of 2026-04 — SDK falls back to bearer-only sessions |
| Firefox | ⚠️ Behind flag |
| Mobile Chrome / Android WebView | ✅ |
| Mobile Safari | ⚠️ See above |
Unsupported browsers continue to work with standard bearer-token sessions. Enforcement of DBSC per user-agent is a dashboard policy.
Error codes
| HTTP | Code | Cause |
|---|---|---|
400 | dbsc_invalid_jwk | public_key_jwk is malformed or missing coordinates. |
400 | dbsc_algorithm_unsupported | algorithm is not "ES256". |
400 | dbsc_origin_mismatch | origin does not match the application’s rpId. |
401 | dbsc_signature_invalid | Challenge or PoP JWT signature failed verification. |
404 | dbsc_session_not_found | sec_session_id unknown or expired. |
410 | dbsc_session_revoked | Session was revoked after a prior failed verification. |
501 | not_implemented | DBSC_ENABLED is false in this deployment. |
Security guarantees
- Private key is non-exportable. Chrome binds to the OS TPM or Secure Enclave. Extraction requires physical possession and vendor-level compromise.
- Every refresh is fresh. PoP JWTs include a
jti(unique per refresh) andiat/nbfclaims. Replay detection is built in. - Atomic revocation. A failed verification marks the session for revocation via Redis
SET NX. Subsequent refreshes return410 Gone. - Origin-bound. The
originclaim in both registration and PoP must match your configuredrpId. A malicious extension on a different origin cannot produce a valid PoP. - Separate from authentication. DBSC is orthogonal to passkey login — you get phishing-resistant login and session integrity as independent layers.
Operational guidance
- Pair with passkeys. DBSC protects the session; passkeys protect the login. Each layer fails open without the other — deploy both.
- Monitor
dbsc_signature_invalid. Sustained failures for a single user can indicate malware on their device. Add it to your SIEM feed. - Tune
refresh_interval_ms. The default 10 minutes balances UX and security. Treasury or admin contexts can safely go to 2–5 minutes. - Log
sec_session_id. Include it alongside user IDs in your app-side logs — it’s your index into session lineage if you ever need to audit.
Related
- Session validation
- Backend validation
- Security
- DBSC W3C draft — external reference