We’ve open-sourced dbsc-php, a small PHP library that makes it easier to deploy Device Bound Session Credentials and turn stolen session cookies into something far less useful. It's MIT-licensed, pure-PHP, and available on Packagist now!

What is DBSC?

If you'd like to know more about DBSC, you should start with my blog post Device Bound Session Credentials: Making Stolen Cookies Useless as that will cover everything you need to know. In short, DBSC lets a browser bind a session cookie to a device-held private key, so a stolen cookie alone is no longer enough to use the session elsewhere.

Alongside open-sourcing this library for the community, we're also running a beta of DBSC at Report URI using this very code, so check it out.

Why we built it

We deployed DBSC on Report URI and quickly found that the gap between "what the spec says" and "how do we do that" is wide enough to fall into. Several behaviours only surface once you're integrating against a real browser, and getting them subtly wrong means enforcement silently does nothing — leaving you with exactly the stolen-cookie hole DBSC exists to close.

Rather than keep those hard-won corrections to ourselves, we've packaged them up. The library is around 700 lines with zero dependencies beyond ext-openssl and ext-json — small enough to audit in one sitting. The crypto is deliberately minimal: ES256 only, signature plus a single-use challenge nonce.

What we got wrong (so you don't have to)

The library is useful, but the wire-protocol notes in the README are where a lot of the hard-won implementation value lives. A few of the corrections baked into the library:

  • Registration is single-phase; refresh is two-phase (a 403 with a challenge, then a 200). That's the opposite of how the spec reads at first glance.
  • Both the cookie value and the challenge must rotate on every refresh. Re-emit the same cookie value and Chrome decides no refresh happened and terminates
    the session.
  • No Secure-Session-Challenge on the registration response, or Chrome reports a Challenge Error.
  • challengeTtl must exceed cookieMaxAge so a challenge cached just before cookie expiry is still valid when it's used. The Config constructor enforces this
    for you.

There's also one non-obvious correctness requirement that bit us in production: keep DBSC state in its own dedicated key space, keyed by session id — never inside a read-modify-written shared session blob. We originally stored it in the PHP session, where the post-login navigation races the registration POST, both rewrite the whole blob last-writer-wins, and the binding gets clobbered. Enforcement then silently no-ops. StoreInterface documents the requirement; back it with Redis or a table and you're fine.

Framework-agnostic by design

The library never touches a superglobal, sends a header, or sets a cookie. Every operation takes a RequestContext you build from your framework's request and returns a DbscResponse you apply to your framework's response. Storage is yours — implement StoreInterface against whatever you already run (an InMemoryStore is bundled for tests and the demo).

use ReportUri\Dbsc{Config, DbscServer};

$dbsc = new DbscServer(new Config(cookieName: '__Host-myapp_dbsc'), $myStore);

A complete reference front controller lives in _test/server.php, and there's a self-contained test harness that generates a real EC P-256 device key, builds the JWTs exactly as Chrome does, and drives the full register/refresh/enforce/revoke flow plus the attack cases — wrong device key, wrong or expired challenge, stale cookie, alg=none.

Getting Started

DBSC is one of the most meaningful upgrades to session security in years, and the cost of adopting it is genuinely low. If you're running PHP and want to start binding sessions to devices, this should save you a lot of effort. Issues and PRs welcome.

Packagist: report-uri/dbsc-php
Source & docs: https://github.com/report-uri/dbsc-php
The spec: w3c/webappsec-dbsc