PHPackages                             report-uri/dbsc-php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. report-uri/dbsc-php

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

report-uri/dbsc-php
===================

A small, framework-agnostic PHP server library for Device Bound Session Credentials (DBSC)

v0.1.4(2mo ago)652MITPHP &gt;=8.1.0

Since May 16Compare

[ Source](https://github.com/report-uri/dbsc-php)[ Packagist](https://packagist.org/packages/report-uri/dbsc-php)[ Docs](https://github.com/report-uri/dbsc-php)[ RSS](/packages/report-uri-dbsc-php/feed)WikiDiscussions Synced 3w ago

READMEChangelog (5)DependenciesVersions (6)Used By (0)

[![Licensed under the MIT License](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](https://github.com/report-uri/dbsc-php/blob/main/LICENSE)[![PHP 8.1+](https://camo.githubusercontent.com/f8d3a5ff01aa798380a3924c1dd664ff2690793107550a190233923694d1d4b7/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312b2d677265656e2e737667)](https://php.net)

dbsc-php
========

[](#dbsc-php)

*A small, framework-agnostic PHP server library for Device Bound Session Credentials (DBSC).*

[DBSC](https://github.com/w3c/webappsec-dbsc) cryptographically binds an authenticated session to a hardware-backed device key (TPM / secure enclave). A stolen session cookie can no longer be replayed from another device: the short-lived bound cookie expires every few minutes and is only refreshable by signing a server challenge with a private key that never leaves the device.

It is pure HTTP headers — **no JavaScript, no frontend assets, no database tables required**. Non-DBSC browsers simply ignore the registration header and continue on normal cookie auth, so enabling it cannot lock anyone out.

This library is extracted from [Report URI](https://report-uri.com)'s production DBSC integration ([report-uri/passkeys-php](https://github.com/report-uri/passkeys-php) is its passkeys sibling). It carries the wire-protocol corrections that only surface when integrating against a real browser — see [Wire-protocol notes](#wire-protocol-notes).

Design
------

[](#design)

- **Zero dependencies** beyond `ext-openssl` / `ext-json`. ~700 lines, auditable in one sitting.
- **Framework-agnostic.** The library never reads superglobals, 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.** You implement `StoreInterface` (Redis, a table, …). An `InMemoryStore` is bundled for tests and the demo.
- **The crypto is deliberately minimal** — ES256 only, signature + single-use challenge nonce. See the class docblock on `JwtVerifier` for why `iat`/`exp`/`iss`/`aud` are intentionally *not* checked.

Installation
------------

[](#installation)

```
composer require report-uri/dbsc-php
```

Autoloads under PSR-4 as `ReportUri\Dbsc\`. The entry point is `ReportUri\Dbsc\DbscServer`.

```
use ReportUri\Dbsc\{Config, DbscServer};

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

Flow
----

[](#flow)

```
        BROWSER (DBSC-capable)              YOUR APP
   ----------------------------------------------------------------
   GET  /login  (full auth done) ----->  buildRegistrationHeaderResponse()
                                  register()
                                  issueRefreshChallenge()
                                  refresh()
                                  enforcement gate (see below)
   GET  /logout                  ----->  revoke()

```

A complete reference front controller is in [`_test/server.php`](_test/server.php). DBSC is browser-native (no JS API to script), so exercise it with a DBSC-capable browser over HTTPS.

Enforcement gate
----------------

[](#enforcement-gate)

The library exposes the primitives but does **not** run the gate itself — *where* you enforce depends on your routing. The recommended policy (also in `_test/server.php`):

```
$binding = $dbsc->getBinding($ctx);

if ($binding === null) {
    // Never registered: unsupported browser, or not yet. Degrade to normal cookie auth.
    // (Do NOT block here — this is what makes locking out a Firefox user impossible.)
}

$mustCheck = $dbsc->isDocumentRequest($ctx) || !$dbsc->isWithinRegistrationGrace($binding);
if ($mustCheck && !$dbsc->boundCookieMatches($binding, $ctx)) {
    // Bound session, bad/absent device cookie -> revoke + log the user out, redirect to login.
    $resp = $dbsc->revoke($ctx, enforcementTerminated: true);
}
```

Enforce on document loads **and** on subresources past the registration grace — *not* document-only, which would let a stolen cookie exfiltrate via XHR within the cookie lifetime. Skip the gate on the `/dbsc/*` endpoints themselves.

Storage
-------

[](#storage)

Key DBSC state by your **stable session id**, in a **dedicated key space** — never in a read-modify-written shared session blob.

> This is the one non-obvious correctness requirement. Report URI shipped DBSC with state in the PHP session blob; the post-login navigation races the `/dbsc/register` POST, both rewrite the whole blob last-writer-wins, the binding is clobbered, and enforcement silently no-ops — leaving exactly the stolen-cookie hole DBSC exists to close. `StoreInterface` documents the requirements; back it with Redis or a table keyed by session id.

Pending registrations expire on the challenge TTL; bindings expire with the session lifetime.

Wire-protocol notes
-------------------

[](#wire-protocol-notes)

Baked into this library from integration testing against real Chrome — change with care:

- **Registration is single-phase; refresh is two-phase** (403 + challenge, then 200). This is the opposite of how the spec reads at first glance.
- **The *first* refresh can optionally be made single-phase.** Steady-state refreshes already are (every 200 hands back the next challenge). Call `advertiseRefreshChallenge($binding, $ctx)` on an ordinary authenticated *document* response in the registration→first-refresh window and it attaches the seed `Secure-Session-Challenge` once (recording a one-way mark on the binding so later responses stay silent), so the browser holds a challenge when its first `/dbsc/refresh` fires and skips the 403. Spec-mandated: never attach this to the registration response (§9.2.1/§8.7 — the `id` must name an already-existing session); the method takes a `Binding`, which only exists post-registration, so misuse is structurally impossible. If the single delivery is missed the browser simply falls back to the two-phase path — no regression. A reactive 403 racing the advertised value is covered by single-depth challenge overlap (the immediately-previous challenge is accepted in `refresh()` until its own TTL), mirroring the bound-cookie overlap.
- **No `Secure-Session-Challenge` on the registration response** — Chrome reports a Challenge Error. The first refresh-flow 403 issues the challenge; the binding seeds an internal one only to stay valid until then.
- **Both the cookie value and the challenge must rotate on every refresh.** Re-emitting the existing cookie value makes Chrome treat it as "no refresh happened" and terminate.
- **`Secure-Session-Challenge` must carry the `id` sf-parameter** naming the session.
- **`challengeTtl` must exceed `cookieMaxAge`** (the `Config` constructor enforces this) so a challenge the browser cached just before cookie expiry is still valid when it is used.
- **The bound cookie uses `__Host-`**, so `include_site` is `false` (no subdomain span).

Tests
-----

[](#tests)

```
php _test/run-tests.php
```

A self-contained harness (no PHPUnit): it 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/expired challenge, stale cookie, `alg=none`).

License
-------

[](#license)

MIT — see [LICENSE](LICENSE). © 2026 Report-URI Ltd.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance86

Actively maintained with recent releases

Popularity17

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

5

Last Release

66d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/5352840?v=4)[Scott Helme](/maintainers/ScottHelme)[@ScottHelme](https://github.com/ScottHelme)

---

Tags

securityAuthenticationsessiondbscdevice-bound-session-credentialscookie-theft

### Embed Badge

![Health badge](/badges/report-uri-dbsc-php/health.svg)

```
[![Health](https://phpackages.com/badges/report-uri-dbsc-php/health.svg)](https://phpackages.com/packages/report-uri-dbsc-php)
```

###  Alternatives

[hwi/oauth-bundle

Support for authenticating users using both OAuth1.0a and OAuth2 in Symfony.

2.4k22.3M81](/packages/hwi-oauth-bundle)[lusitanian/oauth

PHP 7.2 oAuth 1/2 Library

1.1k24.0M133](/packages/lusitanian-oauth)[scheb/2fa

Two-factor authentication for Symfony applications (please use scheb/2fa-bundle to install)

585684.2k1](/packages/scheb-2fa)[alajusticia/laravel-logins

Session management in Laravel apps, user notifications on new access, support for multiple separate remember tokens, IP geolocation, User-Agent parser

2014.5k](/packages/alajusticia-laravel-logins)[rinvex/authy

Rinvex Authy is a simple wrapper for Authy TOTP API, the best rated Two-Factor Authentication service for consumers, simplest 2fa Rest API for developers and a strong authentication platform for the enterprise.

36197.6k1](/packages/rinvex-authy)[iignatov/lightopenid

Lightweight PHP5 library for easy OpenID authentication.

10528.2k7](/packages/iignatov-lightopenid)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
