Security
Keybound
Steal someone's session cookie and you are them. Every app, every permission, every piece of data they have access to. I built Keybound to stop the most common way that happens, copied cookies from infostealer logs and disk dumps, by binding browser sessions to a device key that cannot leave the browser. TypeScript, zero dependencies, works with any Node.js framework.
Session cookies are the weakest link in most web apps. Someone dumps a Chrome profile, grabs a backup, catches an infostealer log. Now they have a valid session token. They drop it into their own browser and the server sees a legitimate logged-in user. MFA already happened. IP checks mean nothing against residential proxies. The token is all it takes.
Keybound adds one check. Before a sensitive action, the server challenges the browser to prove it holds a private key that was created during login. The key lives in IndexedDB as a Web Crypto nonextractable key. The browser can sign with it, but cannot export the raw bytes. Stolen cookies, different machine, no key. The proof fails, the action is denied.
How it works
Enrollment
Browser creates a P-256 key pair. Private key stays in IndexedDB as a CryptoKey flagged nonextractable. Public key goes to the server, linked to the session and a device ID.
Challenge and proof
User hits a protected action. Server issues a challenge tied to that session, that device, and that purpose. Browser signs it with the enrolled private key and returns the signature.
Verification
Server loads the enrolled public key from its own database, verifies the HMAC and the P-256 signature, and checks that the challenge has not been consumed yet. The consume step is atomic. One conditional delete, one winner. Challenge reuse is blocked at the storage layer, not in application logic.
What it protects against
Cookie replay from a different machine. Before and after:
1. User logs in
2. Browser stores session cookie
3. Attacker copies cookie from disk dump or infostealer log
4. Attacker sends the cookie from another machine
5. Server sees a valid session token -- lets them in
1. User logs in
2. Browser creates nonextractable P-256 key
3. Server stores the matching public key
4. Attacker copies the cookies
5. Server asks for a fresh signed challenge
6. Attacker has cookies but not the browser key -- blocked
The limits
Keybound does not stop an attacker who can run JavaScript inside the real browser. XSS, malicious extensions, and malware on the live machine can call signChallenge because the real key is present. This is true of any browser-based proof system and there is no way around it.
It also does not stop a compromised server, a stolen server secret, or a database read by an attacker. Those are separate threat models with their own mitigations.
What Keybound does is raise the floor for the most common attack: stolen cookies. Right now that is a copy-paste operation. With Keybound, it requires live code execution inside the victim's browser. For infostealer logs, profile dumps, and leaked backups, the attack stops cold.
Install
TypeScript, ESM, no runtime dependencies.
npm install keybound
Node.js 20+. Works with Express, Fastify, Next.js, Hono, and plain Node.
Usage
Server side:
import { createKeybound } from "keybound";
export const keybound = createKeybound({
secret: process.env.KEYBOUND_SECRET,
preset: "default"
});
// Issue a challenge
const issued = keybound.issueChallenge({
sessionId,
deviceId,
publicKey,
purpose: "session:renew"
});
// Verify the proof
const result = await keybound.verifyAndConsumeProof({
store: challengeStore,
sessionId,
deviceId,
publicKey,
purpose: "session:renew",
challengeId,
challenge,
signature
});
if (!result.ok) {
// Deny or step up
}
Browser side:
import { getOrCreateKeyboundBrowserKey } from "keybound/browser";
const deviceKey = await getOrCreateKeyboundBrowserKey();
// Enroll with your server
await fetch("/keybound/enroll", {
method: "POST",
body: JSON.stringify({ publicKey: deviceKey.publicKey })
});
// Sign a challenge
const signature = await deviceKey.signChallenge(issued.challenge);
MIT license. On npm and GitHub.