spomky-labs/dbsc-bundle
Symfony bundle for implementing Device Bound Session Credentials (DBSC), binding sessions to device-held keys to reduce cookie theft and session hijacking. Provides configuration, endpoints, and integration with Symfony security for stronger, phishing-resistant session protection.
spomky-labs/dbsc-bundle is a Symfony bundle implementing the W3C Device Bound Session Credentials (DBSC) spec — the browser-side mechanism that binds a long-lived session cookie to a TPM-backed key pair. If you're in a Laravel app, you won't composer require and register a service provider: treat it as a reference implementation and port the two endpoints (registration + refresh) and the header emission logic into middleware/controllers. The spec surface you must reproduce is small: emit Secure-Session-Registration, verify the proof JWS, issue/rotate the bound cookie.composer require spomky-labs/dbsc-bundle, enable the bundle, then configure per firewall. Look first at the bundle's configuration reference for the per-firewall keys, since almost every feature in 0.3.0 is a firewall option.session_lifetime, and the cookie attributes; leave include_site off initially.authorization round-trip (new in 0.3.0). DeviceBoundSessionBadge::setAuthorization($value) adds an authorization parameter to Secure-Session-Registration, binds it into the challenge, and verifies the registration proof echoes it back unchanged. Use it to carry a value only your backend can vouch for — a signed tenant/user claim, an enrollment nonce, a "this registration came from step 2 of our MFA flow" marker. Pattern: mint a short-lived signed value, stash the expected value keyed by challenge server-side, compare on verification. Treat the echoed value as evidence the browser saw your value, not as authentication by itself.include_site: true when your frontend and refresh endpoint live on sibling subdomains (app.example.com refreshing against auth.example.com). Only widen once you've confirmed every host in scope is equally trusted — a site-scoped cookie is shared wider.allowed_refresh_initiators lists origins permitted to initiate a refresh; it's only emitted in the session config document when non-empty. Add every frontend origin that will call the refresh endpoint. Pair this with proper CORS (Access-Control-Allow-Credentials: true and an explicit origin, not *).200 + continue: true — session refreshed, cookie rotated; keep going.200 + continue: false — session ended gracefully (lifetime elapsed). Clear local state, prompt re-login/re-registration. Do not retry.401 — unknown session. Same user-facing outcome (re-register), but don't let your logs/alerts treat these as equivalent to expiry.fetch() from the browser, often before your session is established. They must be reachable without CSRF tokens and must run before any auth middleware that would reject a request carrying only the bound cookie. In Laravel: put them outside the web group's CSRF, with the DBSC verification as the only guard.Secure-Session-Skipped. The bundle now logs at notice level when a browser reports it skipped DBSC (refresh endpoint unreachable, server error, quota exceeded). Ship a log-based alert on this — it's effectively an uptime/instrumentation signal for your refresh endpoint that you'd otherwise never see, because the browser fails silently.__Host-/__Secure- was wrong and is corrected in 0.3.0. A __Host- name needs secure, path=/ and no domain attribute — the bundle defaults satisfy all three. The default remains unprefixed dbsc_session for compatibility; switch to __Host-dbsc_session if you want the extra hardening, but don't set domain at the same time or the browser drops the cookie silently.aud checking is conditional. Proof verification now checks aud against the endpoint URL when the browser includes it, and tolerates its absence. So aud is defence-in-depth, not a guarantee — never build a security decision on the assumption that a proof is endpoint-bound.jwk protected header first, falling back to the legacy key payload claim. If you have custom verification or tests that assert on the proof structure, update them; if you're mid-rollout, keep the fallback until your browser fleet has moved.200/continue: false with a cleared cookie; unknown sessions still 401 (deliberately, to avoid confirming existence). Your metrics should separate these — a spike in 401s is a bug or an attack; a spike in graceful expiries is just user inactivity.Secure-Session-Registration, then open the profiler panel. If the panel shows no exchange, the request never hit a DBSC endpoint — usually the fetch was blocked by CORS, CSRF, or a redirect. The decoded JWS view tells you instantly whether the browser is minting proofs at all (i.e. whether the user's browser supports DBSC).Secure-Session-Skipped signal is your hint that some users are on that path.How can I help you explore Laravel packages today?