dbsc-toolkit 2.9.9 → 2.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -257
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,62 +1,66 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/banner.png" width="880" alt="DBSC Toolkit">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
[](https://github.com/SulimanAbdulrazzaq/dbsc-toolkit/actions/workflows/ci.yml)
|
|
5
|
-
[](./LICENSE)
|
|
6
|
-
[](https://nodejs.org)
|
|
5
|
+
<h1 align="center">DBSC Toolkit</h1>
|
|
7
6
|
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
|
|
11
|
-
>
|
|
12
|
-
> Adapters: Express, Fastify, Hono, Next.js. Storage: in-memory, Redis, Postgres.
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Stop stolen session cookies from being replayed on another device.</strong><br>
|
|
9
|
+
W3C Device Bound Session Credentials for Node.js — with a Web Crypto polyfill so every browser is covered, not just Chrome.
|
|
10
|
+
</p>
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/dbsc-toolkit"><img src="https://img.shields.io/npm/v/dbsc-toolkit.svg" alt="npm"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/dbsc-toolkit"><img src="https://img.shields.io/npm/dm/dbsc-toolkit.svg" alt="downloads"></a>
|
|
15
|
+
<a href="https://github.com/SulimanAbdulrazzaq/dbsc-toolkit/actions/workflows/ci.yml"><img src="https://github.com/SulimanAbdulrazzaq/dbsc-toolkit/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
16
|
+
<img src="https://img.shields.io/badge/types-TypeScript-blue.svg" alt="TypeScript">
|
|
17
|
+
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/dbsc-toolkit.svg" alt="License"></a>
|
|
18
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/dbsc-toolkit.svg" alt="Node"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
## The problem
|
|
22
|
+
|
|
23
|
+
An attacker steals a session cookie — XSS, infostealer malware, a leaked log, a compromised proxy. The moment they have it, they paste it into their own browser and they're you. `HttpOnly` didn't matter. `Secure` didn't matter. Rotating the token only shortens the window; it doesn't close it.
|
|
24
|
+
|
|
25
|
+
## The solution
|
|
26
|
+
|
|
27
|
+
DBSC binds the session to a cryptographic key generated on the user's device at login. The cookie is still stealable — but every refresh, and every guarded request, needs a signature from that key. An attacker on another machine has nothing to sign with. The replay 403s.
|
|
20
28
|
|
|
21
|
-
// Guard sensitive routes:
|
|
22
|
-
app.post("/payment", express.raw({ type: "*/*" }), requireProof(), paymentHandler);
|
|
23
29
|
```
|
|
30
|
+
Browser Server
|
|
31
|
+
─────── ──────
|
|
32
|
+
sign in ─────────────────────────────▶ issue session + Secure-Session-Registration
|
|
33
|
+
generate keypair (TPM / IndexedDB)
|
|
34
|
+
register public key ──────────────────▶ store key, bind to session
|
|
35
|
+
◀────────────────── bound session cookie
|
|
36
|
+
...later, on a guarded request:
|
|
37
|
+
sign challenge with private key ──────▶ verify signature → 200
|
|
38
|
+
stolen cookie, no key ──────▶ 403 ✋
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<img src="./assets/demo.gif" width="720" alt="Login → bound → stolen-cookie replay → 403">
|
|
43
|
+
</p>
|
|
24
44
|
|
|
25
|
-
|
|
45
|
+
### What a stolen cookie gets the attacker
|
|
26
46
|
|
|
27
|
-
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="./assets/what-stealer-sees.png" width="760" alt="Attacker replays the stolen cookie from another machine and the request is rejected">
|
|
49
|
+
</p>
|
|
28
50
|
|
|
29
|
-
|
|
30
|
-
- [Quick start](#quick-start)
|
|
31
|
-
- [Live demo](#live-demo)
|
|
32
|
-
- [Why dbsc-toolkit](#why-dbsc-toolkit)
|
|
33
|
-
- [Comparison](#comparison)
|
|
34
|
-
- [Why not JWT?](#why-not-just-use-jwt)
|
|
35
|
-
- [How it works](#how-it-works)
|
|
36
|
-
- [Who is this for?](#who-is-this-for)
|
|
37
|
-
- [Adding to an existing app](#adding-to-an-existing-app)
|
|
38
|
-
- [Protect your routes](#protect-your-routes)
|
|
39
|
-
- [Production checklist](#production-checklist)
|
|
40
|
-
- [API](#subpath-imports)
|
|
41
|
-
- [Security model](#security-model)
|
|
42
|
-
- [Going deeper](#going-deeper)
|
|
51
|
+
The cookie copies fine. The key doesn't — it never left the user's device. So the replayed request fails the proof check.
|
|
43
52
|
|
|
44
|
-
##
|
|
53
|
+
## Live demo
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
**<https://dbsc-toolkit.onrender.com/>** — sign in, then hit the **"simulate stolen cookie"** button. It fires a bare request with the bound cookie and no proof, and comes back `403 PROOF_MISSING`. That's the whole library in one button.
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
Chromium 145+ lands on `tier: "dbsc"` (TPM-backed); Firefox/Safari land on `tier: "bound"` (Web Crypto polyfill). Same guard either way.
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
## Why DBSC Toolkit exists
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
- Storage adapters for memory, Redis and Postgres. Swap by changing one import.
|
|
56
|
-
- `requireProof()` — a per-request signature guard that works on every browser and defends against MITM body substitution via the signed body hash.
|
|
57
|
-
- Replay cache (since v2.8) that rejects a captured-proof replay inside the timestamp window. Opt-in; you wire `RedisReplayCache` when you want it.
|
|
58
|
-
- Multi-subdomain binding (since v2.9) via `cookieScope: "site"` for apps split across `app.example.com` + `api.example.com`.
|
|
59
|
-
- Apache 2.0.
|
|
61
|
+
Native DBSC currently ships only on Chromium-based browsers. That leaves Firefox, Safari, mobile, and older Chromium users on plain cookies — defeating the point if you're trying to stop cookie theft across your whole user base.
|
|
62
|
+
|
|
63
|
+
DBSC Toolkit extends device-bound sessions to those browsers through a Web Crypto polyfill, so every user gets the same per-request guard. Today it's one of the few open-source implementations providing cross-browser DBSC-style protection for Node.js.
|
|
60
64
|
|
|
61
65
|
## Install
|
|
62
66
|
|
|
@@ -64,16 +68,26 @@ What you actually get:
|
|
|
64
68
|
npm install dbsc-toolkit
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
Framework and storage drivers are optional peer deps — install only what you use:
|
|
68
72
|
|
|
69
73
|
```sh
|
|
70
74
|
npm install express ioredis # Express + Redis
|
|
71
75
|
npm install fastify @fastify/cookie pg # Fastify + Postgres
|
|
72
76
|
```
|
|
73
77
|
|
|
74
|
-
##
|
|
78
|
+
## Integration in 3 lines
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
Drop into an existing Express app — no rewrite of your login or session store:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const dbsc = createDbsc({ storage }); // 1. configure once
|
|
84
|
+
dbsc.install(app); // 2. mount protocol routes + SDK
|
|
85
|
+
await dbsc.bind(res, sessionId, { userId }); // 3. bind, in your login route
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then guard sensitive routes with `requireProof()`. The full picture:
|
|
89
|
+
|
|
90
|
+
## Quick start
|
|
77
91
|
|
|
78
92
|
```ts
|
|
79
93
|
import express from "express";
|
|
@@ -82,21 +96,20 @@ import { createDbsc } from "dbsc-toolkit/express";
|
|
|
82
96
|
import { MemoryStorage } from "dbsc-toolkit/storage/memory";
|
|
83
97
|
|
|
84
98
|
const app = express();
|
|
85
|
-
app.use(express.json());
|
|
99
|
+
app.use(express.json());
|
|
86
100
|
|
|
87
|
-
const dbsc = createDbsc({ storage: new MemoryStorage() }); //
|
|
88
|
-
dbsc.install(app);
|
|
101
|
+
const dbsc = createDbsc({ storage: new MemoryStorage() }); // Redis/Postgres in prod
|
|
102
|
+
dbsc.install(app); // mounts protocol routes + SDK
|
|
89
103
|
|
|
90
104
|
app.post("/login", async (req, res) => {
|
|
91
|
-
await dbsc.bind(res, randomUUID(), { userId: req.body.username });
|
|
105
|
+
await dbsc.bind(res, randomUUID(), { userId: req.body.username }); // the one new line
|
|
92
106
|
res.json({ ok: true });
|
|
93
107
|
});
|
|
94
108
|
|
|
95
|
-
app.
|
|
96
|
-
app.listen(3000);
|
|
109
|
+
app.post("/payment", express.raw({ type: "*/*" }), dbsc.requireProof(), payHandler);
|
|
97
110
|
```
|
|
98
111
|
|
|
99
|
-
Load the polyfill in your HTML so
|
|
112
|
+
Load the polyfill in your HTML so non-Chromium browsers reach `tier: "bound"`:
|
|
100
113
|
|
|
101
114
|
```html
|
|
102
115
|
<script type="module">
|
|
@@ -105,249 +118,117 @@ Load the polyfill in your HTML so Firefox / Safari / older Chromium can reach `t
|
|
|
105
118
|
</script>
|
|
106
119
|
```
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
### Common failure modes
|
|
111
|
-
|
|
112
|
-
- **`tier` always reads `"none"` on Chromium 146+** — running on plain HTTP (DBSC needs HTTPS), or `dbsc.install(app)` was never called. `install()` sets `trust proxy` and parses cookies, so the old "middleware order" class of bug is gone.
|
|
113
|
-
- **Chrome loops registration** — storage got wiped. Move off `MemoryStorage` to Redis or Postgres before deploying anywhere that restarts. I learned this the hard way on Render free tier.
|
|
114
|
-
- **Tier flips back to `"none"` right after login** — the race between `/login` returning and the browser running `POST /dbsc/registration`. Poll `/me` for ~1 s after login, or await the bound-SDK outcome promise. The demo wires both — see [examples/express/src/server.js](./examples/express/src/server.js).
|
|
115
|
-
- **Firefox / Safari still on `"none"`** — the `<script type="module">` tag is missing. `install()` serves the SDK at `/dbsc-client`; you still load it on the page.
|
|
116
|
-
|
|
117
|
-
Walk-through: [docs/getting-started.md](./docs/getting-started.md).
|
|
121
|
+
Full walk-through, failure modes, and migration timeline: **[docs/getting-started.md](./docs/getting-started.md)** and **[docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md)**.
|
|
118
122
|
|
|
119
123
|
### Not on Express?
|
|
120
124
|
|
|
121
|
-
|
|
125
|
+
The protocol lives in a framework-agnostic core — plain functions plus a `StorageAdapter`, no HTTP-layer assumptions. The Express/Fastify/Hono/Next.js adapters are thin wrappers. Build on the core directly for any runtime:
|
|
122
126
|
|
|
123
127
|
```ts
|
|
124
128
|
import {
|
|
125
|
-
handleRegistration,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
buildRegistrationHeader,
|
|
131
|
-
type StorageAdapter, // the only interface you implement
|
|
129
|
+
handleRegistration, handleRefresh,
|
|
130
|
+
handleBoundRegistration, handleBoundRefresh,
|
|
131
|
+
verifyBoundProof, // your requireProof() equivalent
|
|
132
|
+
issueChallenge, buildRegistrationHeader,
|
|
133
|
+
type StorageAdapter, // the only interface you implement
|
|
132
134
|
} from "dbsc-toolkit";
|
|
133
135
|
```
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
## Live demos
|
|
138
|
-
|
|
139
|
-
| URL | Stack | Source |
|
|
140
|
-
|---|---|---|
|
|
141
|
-
| <https://dbsc-toolkit.onrender.com/> | Express + raw `dbsc-toolkit` | [examples/express/](./examples/express/) |
|
|
142
|
-
| <https://dbsc-better-auth-demo.onrender.com/> | Hono + Better Auth + `@dbsc-toolkit/better-auth` | [examples/better-auth/](./examples/better-auth/) |
|
|
143
|
-
|
|
144
|
-
Sign up, log in, click **Check session**. Chromium 145+ lands on `tier: "dbsc"` within a second; Firefox/Safari land on `tier: "bound"` within ~3 seconds. Open DevTools Network and watch the binding flow.
|
|
145
|
-
|
|
146
|
-
## Comparison
|
|
147
|
-
|
|
148
|
-
| | Plain cookies | JWT (bearer) | Native DBSC (Chrome 146+) | **dbsc-toolkit** |
|
|
149
|
-
|----------------------------|:-------------:|:------------:|:-------------------------:|:----------------:|
|
|
150
|
-
| Stops cookie / token replay from another device | ❌ | ❌ | ✅ | ✅ |
|
|
151
|
-
| Works on Chrome / Edge / Brave | ✅ | ✅ | ✅ (Windows + TPM) | ✅ |
|
|
152
|
-
| Works on Firefox | ✅ | ✅ | ❌ | ✅ (polyfill) |
|
|
153
|
-
| Works on Safari | ✅ | ✅ | ❌ | ✅ (polyfill) |
|
|
154
|
-
| Works on mobile / no-TPM | ✅ | ✅ | ❌ | ✅ (polyfill) |
|
|
155
|
-
| Per-request body-hash proof against MITM | ❌ | ❌ | ❌ (TPM key isn't reachable from JS) | ✅ |
|
|
156
|
-
| Captured-proof replay defense | n/a | ❌ | n/a | ✅ (v2.8 replay cache) |
|
|
157
|
-
| Multi-subdomain binding | ✅ (loose) | ✅ (loose) | ❌ (`__Host-` only) | ✅ (v2.9 `cookieScope: "site"`) |
|
|
158
|
-
| Server runtime | any | any | n/a (browser-side) | Node.js ≥ 20 |
|
|
159
|
-
|
|
160
|
-
The native-DBSC column above describes a server that uses the spec without any polyfill. The polyfill key in this library lives in IndexedDB (`extractable: false`) rather than a TPM — a notch weaker against malware reading the browser profile, but it stops every remote-theft scenario the same way native DBSC does.
|
|
161
|
-
|
|
162
|
-
## "Why not just use JWT?"
|
|
163
|
-
|
|
164
|
-
JWTs are bearer tokens. If stolen, they can be replayed from another device. DBSC adds proof-of-possession: the browser has to prove on every refresh (and on every `requireProof()`-guarded request) that it still has the private key bound at login. An attacker who got the token alone has nothing to sign with.
|
|
165
|
-
|
|
166
|
-
It complements your existing auth — passwords, MFA, session cookies, JWTs — rather than replacing any of them. The gap it closes is *replay after issue*.
|
|
167
|
-
|
|
168
|
-
## How it works
|
|
169
|
-
|
|
170
|
-
```
|
|
171
|
-
Login → Key Registration → Cookie Issued → Challenge → Signature Verification → Refresh
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
On login the server responds with a `Secure-Session-Registration` header. The browser generates an ECDSA keypair in the TPM (native DBSC) or IndexedDB (polyfill), POSTs the public key, and gets a short-lived session cookie. Every ~10 minutes the browser re-signs a fresh server challenge to renew the cookie. A copy of the cookie pasted into another device has no key — the next refresh fails, and any route guarded by `requireProof()` returns 403 immediately.
|
|
175
|
-
|
|
176
|
-
Full wire-format walk-through: [HOW-IT-WORKS.md](./HOW-IT-WORKS.md).
|
|
177
|
-
|
|
178
|
-
## Who is this for?
|
|
179
|
-
|
|
180
|
-
Use `dbsc-toolkit` if:
|
|
137
|
+
Complete raw-`http` example: [docs/adapters.md](./docs/adapters.md#writing-your-own-adapter).
|
|
181
138
|
|
|
182
|
-
|
|
183
|
-
- Your users authenticate with session cookies (or JWT bearer tokens).
|
|
184
|
-
- Cookie / token theft is in your threat model — XSS, infostealer malware, leaked logs, compromised proxies.
|
|
185
|
-
- You want coverage on Chrome and Firefox / Safari now, not eventually.
|
|
139
|
+
## How it compares
|
|
186
140
|
|
|
187
|
-
|
|
141
|
+
| | Plain cookies | JWT (bearer) | Native DBSC (Chrome 145+) | **dbsc-toolkit** |
|
|
142
|
+
|---|:---:|:---:|:---:|:---:|
|
|
143
|
+
| Replay-resistant (stolen cookie from another device) | ❌ | ❌ | ✅ | ✅ |
|
|
144
|
+
| Works on Chrome / Edge / Brave | ✅ | ✅ | ✅ (TPM) | ✅ |
|
|
145
|
+
| Works on Firefox / Safari / mobile / no-TPM | ✅ | ✅ | ❌ | ✅ (polyfill) |
|
|
146
|
+
| Per-request body-hash proof vs MITM | ❌ | ❌ | ❌ | ✅ |
|
|
147
|
+
| Captured-proof replay defense | n/a | ❌ | n/a | ✅ (replay cache) |
|
|
148
|
+
| Multi-subdomain binding | loose | loose | ❌ | ✅ (`cookieScope: "site"`) |
|
|
149
|
+
| Better Auth integration | n/a | n/a | n/a | ✅ (plugin) |
|
|
188
150
|
|
|
189
|
-
|
|
190
|
-
- You can't deploy HTTPS — DBSC cookies require `Secure`, and the spec rejects insecure origins.
|
|
191
|
-
- Your backend is Python / Go / Rust / Java / .NET — no port exists yet; you'd implement against the [W3C spec](https://www.w3.org/TR/dbsc/) directly.
|
|
192
|
-
- You need DBSC on iOS / mobile Chrome / ChromeOS *with native key storage* — those platforms don't ship native DBSC. The polyfill still works there; the key just lives in IndexedDB instead of secure hardware.
|
|
151
|
+
DBSC complements your existing auth (passwords, MFA, sessions, JWTs) — it closes *replay after issue*, the gap none of them cover. More: [docs/security/threat-model.md](./docs/security/threat-model.md).
|
|
193
152
|
|
|
194
|
-
##
|
|
153
|
+
## Security at a glance
|
|
195
154
|
|
|
196
|
-
|
|
155
|
+
Defended:
|
|
197
156
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const dbsc = createDbsc({ storage: new RedisStorage(new Redis(process.env.REDIS_URL)) });
|
|
205
|
-
|
|
206
|
-
// 2. install once — mounts the protocol routes, trust proxy, the SDK
|
|
207
|
-
dbsc.install(app);
|
|
208
|
-
|
|
209
|
-
// 3. one line in your existing /login, after the password check
|
|
210
|
-
app.post("/login", async (req, res) => {
|
|
211
|
-
const user = await yourPasswordCheck(req.body); // unchanged
|
|
212
|
-
const sid = await issueYourOwnSession(user.id); // unchanged
|
|
213
|
-
await dbsc.bind(res, sid, { userId: user.id }); // <- the new line
|
|
214
|
-
res.json({ ok: true });
|
|
215
|
-
});
|
|
157
|
+
- ✅ Cookie theft replayed from another device
|
|
158
|
+
- ✅ Stolen bearer tokens (same category)
|
|
159
|
+
- ✅ XSS reading `document.cookie`
|
|
160
|
+
- ✅ Network capture / TLS-stripping proxy
|
|
161
|
+
- ✅ Server log leakage
|
|
162
|
+
- ✅ MITM body substitution (signed body hash)
|
|
216
163
|
|
|
217
|
-
|
|
218
|
-
app.post("/payment", express.raw({ type: "*/*" }), requireProof(), paymentHandler);
|
|
164
|
+
Not defended (be honest about the boundary):
|
|
219
165
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await res.locals.dbsc.revoke(); // <- the new line
|
|
223
|
-
await yourSessionStore.delete(req.cookies.sid); // unchanged
|
|
224
|
-
res.json({ ok: true });
|
|
225
|
-
});
|
|
226
|
-
```
|
|
166
|
+
- ⚠️ On-device malware reading the browser profile — only the `dbsc` tier (TPM) defeats this; the `bound` polyfill key is on disk
|
|
167
|
+
- ⚠️ Browser/OS compromise, rogue extension with `subtle.sign` access
|
|
227
168
|
|
|
228
|
-
|
|
169
|
+
Full STRIDE analysis: [docs/security/threat-model.md](./docs/security/threat-model.md) · best practices: [docs/security/best-practices.md](./docs/security/best-practices.md).
|
|
229
170
|
|
|
230
|
-
|
|
171
|
+
## Protection tiers
|
|
231
172
|
|
|
232
|
-
|
|
173
|
+
Every session carries a `tier`. You don't gate on it directly — `requireProof()` does the enforcement — but it tells you how the binding was achieved on a given browser.
|
|
233
174
|
|
|
234
|
-
|
|
175
|
+
<p align="center">
|
|
176
|
+
<img src="./assets/tier.png" width="760" alt="dbsc (TPM-backed), bound (Web Crypto polyfill), and none">
|
|
177
|
+
</p>
|
|
235
178
|
|
|
236
|
-
|
|
179
|
+
`dbsc` is hardware-backed (TPM / Secure Enclave). `bound` is the Web Crypto polyfill (non-extractable IndexedDB key). `none` is an unbound or stale session. Detail: [HOW-IT-WORKS.md](./HOW-IT-WORKS.md).
|
|
237
180
|
|
|
238
|
-
|
|
181
|
+
## Ecosystem
|
|
239
182
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
export const auth = betterAuth({
|
|
246
|
-
plugins: [dbsc()],
|
|
247
|
-
})
|
|
248
|
-
```
|
|
183
|
+
| Package | What it is |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `dbsc-toolkit` | Core + Express/Fastify/Hono/Next.js adapters, memory/Redis/Postgres storage |
|
|
186
|
+
| `dbsc-toolkit/client` | Browser SDK + Web Crypto polyfill |
|
|
187
|
+
| [`@dbsc-toolkit/better-auth`](./packages/better-auth/) | First-class [Better Auth](https://better-auth.com) plugin — binds every sign-in method automatically |
|
|
249
188
|
|
|
250
189
|
```ts
|
|
251
|
-
//
|
|
252
|
-
import {
|
|
253
|
-
|
|
254
|
-
const dbsc = dbscExpress(auth)
|
|
255
|
-
dbsc.install(app) // mounts /dbsc/*, /dbsc-bound/*, /dbsc-client/*
|
|
256
|
-
app.all("/api/auth/*splat", toNodeHandler(auth))
|
|
257
|
-
app.get("/profile", dbsc.requireProof(), profileHandler)
|
|
190
|
+
// Better Auth — one plugin line, works on every framework:
|
|
191
|
+
import { dbsc } from "@dbsc-toolkit/better-auth" // auth.ts → plugins: [dbsc()]
|
|
258
192
|
```
|
|
259
193
|
|
|
260
|
-
|
|
261
|
-
<script src="/dbsc-client/init.js" type="module"></script>
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Live demo: <https://dbsc-better-auth-demo.onrender.com/>. Full docs in the [package README](./packages/better-auth/README.md).
|
|
265
|
-
|
|
266
|
-
## Protect your routes
|
|
194
|
+
## Roadmap
|
|
267
195
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Full threat boundary, per-framework wiring (Fastify / Hono / Next.js), and the migration timeline: [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md) and [docs/per-request-signing.md](./docs/per-request-signing.md).
|
|
280
|
-
|
|
281
|
-
## Production checklist
|
|
282
|
-
|
|
283
|
-
- [ ] HTTPS enabled — DBSC requires `Secure` cookies; plain HTTP locks every user to `tier: "none"`.
|
|
284
|
-
- [ ] Redis or PostgreSQL storage — `MemoryStorage` is lost on restart; bound browsers loop registration.
|
|
285
|
-
- [ ] `requireProof()` on every authenticated route — read-only public routes need no guard.
|
|
286
|
-
- [ ] `wrapFetch({ signBody: true })` on the client side for those routes (default since v2.8).
|
|
287
|
-
- [ ] Replay cache enabled when relevant — `replayCache: new RedisReplayCache(redis)` in `createDbsc({...})`.
|
|
288
|
-
- [ ] `trust proxy` set — `install()` sets it, but verify nothing in your own app config overrides it.
|
|
289
|
-
- [ ] Polyfill script loaded — the `<script type="module">` tag; without it Firefox/Safari stay on `tier: "none"`.
|
|
196
|
+
- [x] Express, Fastify, Hono, Next.js adapters
|
|
197
|
+
- [x] Memory, Redis, PostgreSQL storage
|
|
198
|
+
- [x] Web Crypto polyfill (Firefox / Safari / mobile / older Chromium)
|
|
199
|
+
- [x] Per-request proof + body signing + replay cache
|
|
200
|
+
- [x] Multi-subdomain binding (`cookieScope: "site"`)
|
|
201
|
+
- [x] Better Auth plugin
|
|
202
|
+
- [ ] Koa / NestJS adapters
|
|
203
|
+
- [ ] Bun / Deno native paths
|
|
204
|
+
- [ ] Third-party security audit
|
|
205
|
+
- [ ] WebAuthn step-up integration
|
|
290
206
|
|
|
291
207
|
## Subpath imports
|
|
292
208
|
|
|
293
209
|
| Import | What it is |
|
|
294
|
-
|
|
295
|
-
| `dbsc-toolkit` | Core types, crypto, protocol
|
|
296
|
-
| `dbsc-toolkit/express` |
|
|
297
|
-
| `dbsc-toolkit/
|
|
298
|
-
| `dbsc-toolkit/hono` | Hono middleware |
|
|
299
|
-
| `dbsc-toolkit/nextjs` | Next.js App Router middleware + handlers |
|
|
300
|
-
| `dbsc-toolkit/client` | Browser SDK with `initBoundDbsc()` for the polyfill |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `dbsc-toolkit` | Core: types, crypto, protocol handlers, framework-agnostic |
|
|
212
|
+
| `dbsc-toolkit/express` · `/fastify` · `/hono` · `/nextjs` | Framework adapters |
|
|
213
|
+
| `dbsc-toolkit/client` | Browser SDK + polyfill |
|
|
301
214
|
| `dbsc-toolkit/storage/{memory,redis,postgres}` | Storage adapters |
|
|
302
215
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
## Protection tiers
|
|
306
|
-
|
|
307
|
-
`tier` is the state of a session — what kind of binding the browser achieved. Don't gate on it directly; gate with `requireProof()` instead and read `tier` only for display.
|
|
308
|
-
|
|
309
|
-
| Tier | Mechanism | Protects against |
|
|
310
|
-
|------|-----------|------------------|
|
|
311
|
-
| `dbsc` | Native W3C DBSC, key in TPM (Windows) or Secure Enclave (macOS) | Cookie theft (XSS, network, logs, paste-to-other-browser) **and** infostealer malware reading the browser profile |
|
|
312
|
-
| `bound` | Web Crypto polyfill, non-extractable ECDSA P-256 key in IndexedDB | Cookie theft. Does not defeat infostealer malware on the user's machine. |
|
|
313
|
-
| `none` | No active / fresh binding | Nothing the cookie itself doesn't already do |
|
|
314
|
-
|
|
315
|
-
Why not gate routes on `tier === "dbsc"`? Because every Firefox and Safari user can only reach `tier: "bound"`, and `requireProof()` already gives those browsers the same per-request guarantee via a signed proof. The one exception is routes whose threat model specifically includes on-device infostealer malware — there, hardware-backed key isolation actually matters, and you accept the trade-off of locking non-Chromium browsers out. More in [docs/security/best-practices.md](./docs/security/best-practices.md).
|
|
316
|
-
|
|
317
|
-
## Security model
|
|
318
|
-
|
|
319
|
-
What this protects against:
|
|
320
|
-
|
|
321
|
-
- Stolen session cookies replayed from another device.
|
|
322
|
-
- Stolen bearer tokens (same category — bearer == portable).
|
|
323
|
-
- Cookie exfiltration via XSS, network capture, log leakage, or proxy leakage.
|
|
324
|
-
|
|
325
|
-
What the Web Crypto polyfill (`tier: "bound"`) does not protect against:
|
|
326
|
-
|
|
327
|
-
- Malware running on the user's machine with access to the browser profile.
|
|
328
|
-
- Browser or OS compromise.
|
|
329
|
-
|
|
330
|
-
Native DBSC (`tier: "dbsc"`) is stronger against local malware because the private key lives inside the TPM or Secure Enclave — software can't extract it even with admin access. The polyfill key is non-extractable from JavaScript, but the encrypted blob lives on disk and a privileged on-device attacker can reach it.
|
|
331
|
-
|
|
332
|
-
None of this replaces HTTPS, input validation, strong passwords, or MFA.
|
|
333
|
-
|
|
334
|
-
## Going deeper
|
|
216
|
+
## Docs
|
|
335
217
|
|
|
336
|
-
- **Concepts
|
|
218
|
+
- **Concepts & protocol:** [HOW-IT-WORKS.md](./HOW-IT-WORKS.md)
|
|
219
|
+
- **Getting started:** [docs/getting-started.md](./docs/getting-started.md)
|
|
220
|
+
- **Add to an existing app:** [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md)
|
|
221
|
+
- **Per-request signing & replay cache:** [docs/per-request-signing.md](./docs/per-request-signing.md)
|
|
337
222
|
- **Bound polyfill wire protocol:** [docs/bound-polyfill.md](./docs/bound-polyfill.md)
|
|
338
|
-
- **Per-request signing (v2.7 dual-key + v2.8 replay cache):** [docs/per-request-signing.md](./docs/per-request-signing.md)
|
|
339
223
|
- **API reference:** [docs/api-reference.md](./docs/api-reference.md)
|
|
340
|
-
- **Adapters (
|
|
341
|
-
- **Storage (
|
|
342
|
-
- **
|
|
343
|
-
- **Deployment (Render / Fly / Vercel / Cloudflare / nginx):** [docs/deployment.md](./docs/deployment.md)
|
|
344
|
-
- **Security best practices:** [docs/security/best-practices.md](./docs/security/best-practices.md)
|
|
345
|
-
- **Threat model:** [docs/security/threat-model.md](./docs/security/threat-model.md)
|
|
224
|
+
- **Adapters (+ build your own):** [docs/adapters.md](./docs/adapters.md)
|
|
225
|
+
- **Storage:** [docs/storage.md](./docs/storage.md) · **Deployment:** [docs/deployment.md](./docs/deployment.md)
|
|
226
|
+
- **Security:** [threat model](./docs/security/threat-model.md) · [best practices](./docs/security/best-practices.md)
|
|
346
227
|
- **Troubleshooting:** [docs/troubleshooting.md](./docs/troubleshooting.md)
|
|
347
228
|
|
|
348
229
|
## Status
|
|
349
230
|
|
|
350
|
-
Verified end-to-end on Chrome 147 / Windows / TPM 2.0. Native DBSC requires Chromium
|
|
231
|
+
Verified end-to-end on Chrome 147 / Windows / TPM 2.0. Native DBSC requires Chromium 145+ on Windows or Apple Silicon macOS; the polyfill covers every browser with Web Crypto + IndexedDB. No third-party security audit yet — see [HOW-IT-WORKS.md#production-readiness](./HOW-IT-WORKS.md#production-readiness).
|
|
351
232
|
|
|
352
233
|
## License
|
|
353
234
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbsc-toolkit",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.11",
|
|
4
4
|
"description": "Server-side Device Bound Session Credentials (DBSC) for Node.js plus a Web Crypto polyfill for Firefox, Safari, and older Chromium. Cookie-theft protection on every modern browser.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|