bro-auth 0.1.3 → 0.2.0

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 CHANGED
@@ -1,169 +1,231 @@
1
1
  # bro-auth
2
2
 
3
- ```
4
- ┌──────────────────────────────────────────────────────────────┐
5
- │ █▄▄ █▀█ █▀█ █▀█ █ █ ▀█▀ █ █ bro-auth │
6
- │ █▄█ █▀▄ █▄█ █▀█ █▄█ █ █▀█ │
7
- ├──────────────────────────────────────────────────────────────┤
8
- │ Stateless JWT · Device Fingerprinting · Zero Replay │
9
- └──────────────────────────────────────────────────────────────┘
10
- ```
11
-
12
- A lightweight, **stateless**, and **high-security** authentication layer that combines JWT tokens with device fingerprinting to prevent token theft and replay attacks—no database required.
3
+ **Stateless JWT authentication with device fingerprint binding and derived signing keys.**
13
4
 
14
5
  [![npm version](https://img.shields.io/npm/v/bro-auth.svg)](https://www.npmjs.com/package/bro-auth)
15
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
16
7
 
17
8
  ---
18
9
 
19
- ## 🎯 Why bro-auth?
10
+ ## Overview
11
+
12
+ `bro-auth` is a Node.js library that binds JWT tokens to browser devices using fingerprinting and derives signing keys from application-provided secrets. This prevents token replay attacks and limits the impact of token theft.
13
+
14
+ **What bro-auth does:**
15
+ - Generates and verifies JWT tokens bound to device fingerprints
16
+ - Derives unique signing secrets per user-device combination using HMAC
17
+ - Provides browser fingerprinting utilities
18
+
19
+ **What bro-auth does NOT do:**
20
+ - Manage secrets or environment variables (your application's responsibility)
21
+ - Prevent XSS attacks (your application's responsibility)
22
+ - Handle login UI or session management (your application's responsibility)
23
+
24
+ ---
20
25
 
21
- Traditional JWT authentication has a critical weakness: **stolen tokens work from any device**. If an attacker intercepts your JWT, they can use it from anywhere until it expires.
26
+ ## Why Use bro-auth?
22
27
 
23
- **bro-auth solves this** by binding tokens to specific devices using browser fingerprinting. Even if a token is stolen, it won't work on a different device.
28
+ Traditional JWT tokens work from any device if stolen. `bro-auth` binds tokens to specific devices, making stolen tokens unusable on different browsers.
24
29
 
25
- ### Key Benefits
30
+ ### Attack Mitigation
26
31
 
27
- - 🔐 **Stateless JWT authentication** - No session storage needed
28
- - 🆔 **Device fingerprint binding** - Tokens only work on the issuing device
29
- - 🚫 **Replay attack protection** - Stolen tokens are useless on other browsers
30
- - **Zero dependencies** - Lightweight core (only `jsonwebtoken` + `crypto-es`)
31
- - 🧩 **Framework agnostic** - Works with Next.js, Express, Fastify, or vanilla Node
32
- - 🌐 **Browser module included** - Easy fingerprint extraction
33
- - 📦 **Production ready** - TypeScript support, comprehensive error handling
32
+ For an attacker to successfully use a stolen token, they would need:
33
+
34
+ 1. The JWT token itself (stolen via XSS, network interception, etc.)
35
+ 2. The user ID (embedded in token)
36
+ 3. The application's access/refresh secrets
37
+ 4. The exact device fingerprint hash (SHA-256)
38
+ 5. The application's server-only pepper (`BRO_AUTH_SECRET_PEPPER`)
39
+
40
+ Even with items 1-4, the attacker cannot:
41
+ - Use the token from a different device (fingerprint mismatch fails verification)
42
+ - Forge new tokens (requires the server-only pepper)
43
+ - Replay the token (derived secret verification fails)
44
+
45
+ ### Security Scope & Limitations
46
+
47
+ **What bro-auth protects against:**
48
+ - Token replay from different devices
49
+ - Token forgery without server secrets
50
+ - Credential stuffing across devices
51
+
52
+ **What bro-auth does NOT protect against:**
53
+ - XSS attacks during active execution (attacker can make requests while JS runs)
54
+ - Full server compromise (if secrets are leaked, all bets are off)
55
+ - Social engineering or phishing attacks
56
+ - Browser fingerprint spoofing (though difficult, it's theoretically possible)
57
+
58
+ **Your application must:**
59
+ - Implement XSS prevention (CSP, input sanitization, etc.)
60
+ - Store tokens securely (HTTP-only cookies for refresh tokens)
61
+ - Protect environment variables and secrets
62
+ - Use HTTPS in production
34
63
 
35
64
  ---
36
65
 
37
- ## 📦 Installation
66
+ ## Installation
38
67
 
39
68
  ```bash
40
69
  npm install bro-auth
41
70
  ```
42
71
 
43
- ```bash
44
- yarn add bro-auth
72
+ ---
73
+
74
+ ## How It Works
75
+
76
+ ### 1. Authentication Flow
77
+
78
+ ```
79
+ Browser Server
80
+
81
+ ├─ Generate fingerprint hash
82
+ │ (Canvas, GPU, User-Agent, etc.)
83
+
84
+ ├─ POST /login ────────────────────────▶ Verify credentials
85
+ │ { username, password, fpHash } │
86
+ │ ├─ Derive signing secret:
87
+ │ │ HMAC(pepper, secret|userId|fpHash)
88
+ │ │
89
+ │ ├─ Sign JWT with derived secret
90
+ │ │ Payload: { sub: userId, fp: fpHash }
91
+ │ │
92
+ │ ◀──────────────────────────────────── Return tokens
93
+ │ { accessToken, refreshToken }
94
+
45
95
  ```
46
96
 
47
- ```bash
48
- pnpm add bro-auth
97
+ ### 2. Verification Flow
98
+
99
+ ```
100
+ Browser Server
101
+
102
+ ├─ GET /api/protected ─────────────────▶ Extract token & fpHash
103
+ │ Headers: │
104
+ │ Authorization: Bearer <token> ├─ Decode token (unsafe)
105
+ │ X-Fingerprint: <fpHash> │ Extract userId from payload
106
+ │ │
107
+ │ ├─ Re-derive signing secret:
108
+ │ │ HMAC(pepper, secret|userId|fpHash)
109
+ │ │
110
+ │ ├─ Verify JWT signature
111
+ │ │ jwt.verify(token, derivedSecret)
112
+ │ │
113
+ │ ├─ Compare fingerprints
114
+ │ │ payload.fp === request.fpHash
115
+ │ │
116
+ │ ◀──────────────────────────────────── Grant access ✓
117
+
49
118
  ```
50
119
 
120
+ ### 3. Why Stolen Tokens Fail
121
+
122
+ ```
123
+ Attacker (Different Device) Server
124
+
125
+ ├─ GET /api/protected ─────────────────▶ Extract token & fpHash
126
+ │ Token: <stolen_token> │
127
+ │ Fingerprint: <attacker_fp_hash> ├─ Decode token
128
+ │ │ payload.fp = <victim_fp_hash>
129
+ │ │
130
+ │ ├─ Derive secret using attacker's FP:
131
+ │ │ HMAC(pepper, secret|userId|attacker_fp)
132
+ │ │
133
+ │ ├─ Verify signature
134
+ │ │ ✗ FAILS - Different derived secret
135
+ │ │
136
+ │ ◀──────────────────────────────────── Reject: "invalid signature"
137
+
138
+ ```
139
+
140
+ **Key insight:** The signing secret is derived from the fingerprint. A different fingerprint produces a different secret, causing signature verification to fail.
141
+
51
142
  ---
52
143
 
53
- ## 🧠 How It Works
144
+ ## Quick Start
54
145
 
55
- ```mermaid
56
- sequenceDiagram
57
- participant Browser
58
- participant Server
59
-
60
- Browser->>Browser: Generate device fingerprint
61
- Browser->>Server: Login with credentials + fingerprint
62
- Server->>Server: Hash fingerprint (SHA-256)
63
- Server->>Server: Create JWT bound to fingerprint hash
64
- Server->>Browser: Return access + refresh tokens
65
-
66
- Browser->>Server: API request with token + fingerprint
67
- Server->>Server: Verify token signature
68
- Server->>Server: Verify fingerprint match
69
- Server->>Browser: Grant access ✅
70
-
71
- Note over Browser,Server: If attacker steals token...
72
-
73
- Browser->>Server: Request from different device
74
- Server->>Server: Fingerprint mismatch detected
75
- Server->>Browser: Reject request ❌
76
- ```
146
+ ### Step 1: Configure Environment Variables
77
147
 
78
- ### The Security Flow
148
+ Create a `.env` file in your application (NOT in bro-auth):
79
149
 
80
- 1. **Client generates a device fingerprint** using browser characteristics (User-Agent, screen, GPU, canvas, etc.)
81
- 2. **Server receives fingerprint** during login and creates a SHA-256 hash
82
- 3. **JWT tokens are bound** to this fingerprint hash in their payload
83
- 4. **Every request is verified** for both token validity and fingerprint match
84
- 5. **Token theft is mitigated** - stolen tokens fail fingerprint verification on different devices
150
+ ```bash
151
+ # Required: Application-owned server-only pepper
152
+ # bro-auth requires this but does NOT provide it
153
+ BRO_AUTH_SECRET_PEPPER=your-random-string-min-32-chars-never-expose
85
154
 
86
- ---
155
+ # Your application's JWT signing secrets
156
+ ACCESS_SECRET=your-access-secret-min-32-chars
157
+ REFRESH_SECRET=your-refresh-secret-min-32-chars
158
+ ```
87
159
 
88
- ## 🚀 Quick Start
160
+ **Important:** These are YOUR application's secrets. `bro-auth` reads them but does not manage or provide them.
89
161
 
90
- ### 1. Browser: Generate Device Fingerprint
162
+ ### Step 2: Browser - Generate Fingerprint
91
163
 
92
164
  ```javascript
93
165
  import { getFingerprint } from "bro-auth/browser";
94
166
 
95
167
  async function handleLogin() {
96
- const fp = await getFingerprint();
168
+ // Generate device fingerprint hash
169
+ const fpHash = await getFingerprint();
170
+
171
+ // fpHash is a string: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
97
172
 
98
- // Send to your login endpoint
99
173
  const response = await fetch("/api/login", {
100
174
  method: "POST",
101
175
  headers: { "Content-Type": "application/json" },
102
176
  body: JSON.stringify({
103
177
  username: "user@example.com",
104
- password: "password",
105
- fingerprint: fp.hash
178
+ password: "password123",
179
+ fingerprint: fpHash
106
180
  })
107
181
  });
108
182
 
109
183
  const { accessToken, refreshToken } = await response.json();
110
- // Store tokens securely
111
- }
112
- ```
113
-
114
- **Fingerprint output example:**
115
-
116
- ```json
117
- {
118
- "hash": "53ff76d8a4c21b9c8e3f1a7d9e2c4b5a8f1e3d6c9b2a5e8f1d4c7b0a3e6f9c2696",
119
- "raw": "Mozilla/5.0|1920x1080|ANGLE (Intel, Mesa Intel UHD)|canvas_data...",
120
- "components": {
121
- "userAgent": "Mozilla/5.0 (X11; Linux x86_64)...",
122
- "screenResolution": "1920x1080",
123
- "gpu": "ANGLE (Intel, Mesa Intel UHD Graphics 620)",
124
- "canvas": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
125
- "timezone": "America/New_York"
126
- }
184
+
185
+ // Store tokens (see security best practices)
186
+ sessionStorage.setItem("accessToken", accessToken);
127
187
  }
128
188
  ```
129
189
 
130
- ### 2. Server: Generate Tokens
190
+ ### Step 3: Server - Generate Tokens
131
191
 
132
192
  ```javascript
133
- import { generateTokens } from "bro-auth";
193
+ import { generateTokens } from "bro-auth/core";
134
194
 
135
195
  app.post("/api/login", async (req, res) => {
136
196
  const { username, password, fingerprint } = req.body;
137
197
 
138
- // Verify credentials (your logic here)
139
- const user = await verifyCredentials(username, password);
198
+ // 1. Verify credentials (your logic)
199
+ const user = await authenticateUser(username, password);
140
200
  if (!user) {
141
201
  return res.status(401).json({ error: "Invalid credentials" });
142
202
  }
143
203
 
144
- // Generate tokens bound to device fingerprint
145
- const { accessToken, refreshToken } = generateTokens({
146
- userId: user.id,
147
- fingerprintHash: fingerprint,
148
- accessSecret: process.env.ACCESS_SECRET,
149
- refreshSecret: process.env.REFRESH_SECRET,
150
- accessExpiresIn: "15m", // Optional: default 15m
151
- refreshExpiresIn: "7d" // Optional: default 7d
152
- });
204
+ // 2. Generate device-bound tokens
205
+ const { accessToken, refreshToken } = generateTokens(
206
+ user.id, // userId
207
+ fingerprint, // fpHash from browser
208
+ process.env.ACCESS_SECRET, // your secret
209
+ process.env.REFRESH_SECRET // your secret
210
+ );
153
211
 
154
212
  res.json({ accessToken, refreshToken });
155
213
  });
156
214
  ```
157
215
 
158
- ### 3. Server: Verify Access Token
216
+ ### Step 4: Server - Verify Requests
159
217
 
160
218
  ```javascript
161
- import { verifyAccessToken } from "bro-auth";
219
+ import { verifyAccessToken } from "bro-auth/core";
162
220
 
163
221
  app.get("/api/protected", (req, res) => {
164
222
  const token = req.headers.authorization?.replace("Bearer ", "");
165
223
  const fingerprint = req.headers["x-fingerprint"];
166
224
 
225
+ if (!token || !fingerprint) {
226
+ return res.status(401).json({ error: "Missing credentials" });
227
+ }
228
+
167
229
  const result = verifyAccessToken(
168
230
  token,
169
231
  fingerprint,
@@ -175,15 +237,15 @@ app.get("/api/protected", (req, res) => {
175
237
  }
176
238
 
177
239
  // Access granted
178
- const userId = result.payload.userId;
179
- res.json({ message: "Protected data", userId });
240
+ const userId = result.payload.sub;
241
+ res.json({ message: "Success", userId });
180
242
  });
181
243
  ```
182
244
 
183
- ### 4. Server: Refresh Tokens
245
+ ### Step 5: Server - Refresh Tokens
184
246
 
185
247
  ```javascript
186
- import { verifyRefreshToken, generateTokens, buildRefreshCookie } from "bro-auth";
248
+ import { verifyRefreshToken, generateTokens } from "bro-auth/core";
187
249
 
188
250
  app.post("/api/refresh", (req, res) => {
189
251
  const { refreshToken, fingerprint } = req.body;
@@ -195,20 +257,16 @@ app.post("/api/refresh", (req, res) => {
195
257
  );
196
258
 
197
259
  if (!result.valid) {
198
- return res.status(401).json({ error: result.error });
260
+ return res.status(401).json({ error: "Invalid refresh token" });
199
261
  }
200
262
 
201
263
  // Issue new token pair
202
- const tokens = generateTokens({
203
- userId: result.payload.userId,
204
- fingerprintHash: fingerprint,
205
- accessSecret: process.env.ACCESS_SECRET,
206
- refreshSecret: process.env.REFRESH_SECRET
207
- });
208
-
209
- // Optional: Set refresh token as HTTP-only cookie
210
- const cookie = buildRefreshCookie(tokens.refreshToken);
211
- res.setHeader("Set-Cookie", cookie);
264
+ const tokens = generateTokens(
265
+ result.payload.sub,
266
+ fingerprint,
267
+ process.env.ACCESS_SECRET,
268
+ process.env.REFRESH_SECRET
269
+ );
212
270
 
213
271
  res.json({ accessToken: tokens.accessToken });
214
272
  });
@@ -216,162 +274,310 @@ app.post("/api/refresh", (req, res) => {
216
274
 
217
275
  ---
218
276
 
219
- ## 📚 API Reference
277
+ ## API Reference
220
278
 
221
- ### Browser Module
279
+ ### Browser Module (`bro-auth/browser`)
222
280
 
223
- #### `getFingerprint(): Promise<FingerprintResult>`
281
+ #### `getFingerprint()`
224
282
 
225
- Generates a unique device fingerprint from browser characteristics.
283
+ Generates a SHA-256 hash of browser device characteristics.
226
284
 
227
- **Returns:**
228
- ```typescript
229
- {
230
- hash: string; // SHA-256 hash (send to server)
231
- raw: string; // Raw concatenated fingerprint data
232
- components: { // Individual fingerprint components
233
- userAgent: string;
234
- screenResolution: string;
235
- gpu: string;
236
- canvas: string;
237
- timezone: string;
238
- // ... more components
239
- }
240
- }
285
+ **Returns:** `Promise<string>`
286
+
287
+ **Example:**
288
+ ```javascript
289
+ const fpHash = await getFingerprint();
290
+ // "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
241
291
  ```
242
292
 
243
- ### Server Module
293
+ ---
294
+
295
+ ### Server Module (`bro-auth/core`)
244
296
 
245
- #### `generateTokens(options): TokenPair`
297
+ #### `generateTokens(userId, fpHash, accessSecret, refreshSecret)`
246
298
 
247
- Creates JWT access and refresh tokens bound to a device fingerprint.
299
+ Generates access and refresh tokens bound to a device fingerprint.
248
300
 
249
301
  **Parameters:**
250
- - `userId` (string): Unique user identifier
251
- - `fingerprintHash` (string): Device fingerprint hash from browser
252
- - `accessSecret` (string): Secret key for access tokens
253
- - `refreshSecret` (string): Secret key for refresh tokens
254
- - `accessExpiresIn` (string, optional): Access token TTL (default: "15m")
255
- - `refreshExpiresIn` (string, optional): Refresh token TTL (default: "7d")
256
-
257
- **Returns:**
258
- ```typescript
259
- {
260
- accessToken: string;
261
- refreshToken: string;
262
- }
302
+ - `userId` (string) - Unique user identifier
303
+ - `fpHash` (string) - Device fingerprint hash from browser
304
+ - `accessSecret` (string) - Your application's access token secret
305
+ - `refreshSecret` (string) - Your application's refresh token secret
306
+
307
+ **Returns:** `{ accessToken: string, refreshToken: string }`
308
+
309
+ **Example:**
310
+ ```javascript
311
+ const tokens = generateTokens(
312
+ "user_123",
313
+ fpHash,
314
+ process.env.ACCESS_SECRET,
315
+ process.env.REFRESH_SECRET
316
+ );
263
317
  ```
264
318
 
265
- #### `verifyAccessToken(token, fingerprintHash, secret): VerificationResult`
319
+ ---
320
+
321
+ #### `generateAccessToken(userId, fpHash, secret, expiresIn?)`
322
+
323
+ Generates only an access token.
324
+
325
+ **Parameters:**
326
+ - `userId` (string) - User identifier
327
+ - `fpHash` (string) - Fingerprint hash
328
+ - `secret` (string) - Signing secret
329
+ - `expiresIn` (string, optional) - Expiration time (default: "15m")
330
+
331
+ **Returns:** `string`
332
+
333
+ **Example:**
334
+ ```javascript
335
+ const accessToken = generateAccessToken(
336
+ "user_123",
337
+ fpHash,
338
+ process.env.ACCESS_SECRET,
339
+ "30m"
340
+ );
341
+ ```
342
+
343
+ ---
344
+
345
+ #### `generateRefreshToken(userId, fpHash, secret, expiresIn?)`
346
+
347
+ Generates only a refresh token.
348
+
349
+ **Parameters:**
350
+ - `userId` (string) - User identifier
351
+ - `fpHash` (string) - Fingerprint hash
352
+ - `secret` (string) - Signing secret
353
+ - `expiresIn` (string, optional) - Expiration time (default: "7d")
354
+
355
+ **Returns:** `string`
356
+
357
+ ---
358
+
359
+ #### `verifyAccessToken(token, fpHash, secret)`
360
+
361
+ Verifies an access token and fingerprint binding.
266
362
 
267
- Verifies an access token and its fingerprint binding.
363
+ **Parameters:**
364
+ - `token` (string) - JWT access token
365
+ - `fpHash` (string) - Current device fingerprint hash
366
+ - `secret` (string) - Signing secret
367
+
368
+ **Returns:** `VerificationResult`
268
369
 
269
- **Returns:**
270
370
  ```typescript
271
371
  {
272
372
  valid: boolean;
273
373
  payload?: {
274
- userId: string;
275
- fingerprintHash: string;
276
- iat: number;
277
- exp: number;
374
+ sub: string; // userId
375
+ fp: string; // fingerprint hash
376
+ type: string; // "access"
377
+ iat: number; // issued at timestamp
378
+ exp: number; // expiration timestamp
278
379
  };
279
380
  error?: string;
280
381
  }
281
382
  ```
282
383
 
283
- #### `verifyRefreshToken(token, fingerprintHash, secret): VerificationResult`
384
+ **Possible errors:**
385
+ - `"Invalid token structure"` - Malformed JWT
386
+ - `"invalid signature"` - Token tampered or wrong fingerprint
387
+ - `"jwt expired"` - Token expired
388
+ - `"Invalid token type"` - Not an access token
389
+ - `"Fingerprint mismatch"` - Device fingerprint doesn't match
390
+
391
+ **Example:**
392
+ ```javascript
393
+ const result = verifyAccessToken(token, fpHash, process.env.ACCESS_SECRET);
394
+
395
+ if (result.valid) {
396
+ console.log("User ID:", result.payload.sub);
397
+ } else {
398
+ console.error("Error:", result.error);
399
+ }
400
+ ```
284
401
 
285
- Verifies a refresh token and its fingerprint binding.
402
+ ---
286
403
 
287
- #### `buildRefreshCookie(refreshToken, options?): string`
404
+ #### `verifyRefreshToken(token, fpHash, secret)`
405
+
406
+ Verifies a refresh token and fingerprint binding.
407
+
408
+ **Parameters:** Same as `verifyAccessToken`
409
+
410
+ **Returns:** `VerificationResult`
411
+
412
+ ---
413
+
414
+ #### `buildRefreshCookie(refreshToken, options?)`
288
415
 
289
416
  Generates a secure HTTP-only cookie string for refresh tokens.
290
417
 
291
- **Options:**
292
- - `maxAge` (number): Cookie lifetime in seconds (default: 7 days)
293
- - `domain` (string, optional): Cookie domain
294
- - `sameSite` ("Strict" | "Lax" | "None"): SameSite policy (default: "Strict")
295
- - `secure` (boolean): HTTPS only (default: true)
418
+ **Parameters:**
419
+ - `refreshToken` (string) - The refresh token
420
+ - `options` (object, optional):
421
+ - `maxAge` (number) - Cookie lifetime in seconds (default: 604800 = 7 days)
422
+ - `domain` (string) - Cookie domain
423
+ - `sameSite` ("Strict" | "Lax" | "None") - SameSite policy (default: "Strict")
424
+ - `secure` (boolean) - HTTPS only (default: true)
425
+
426
+ **Returns:** `string` (Set-Cookie header value)
427
+
428
+ **Example:**
429
+ ```javascript
430
+ const cookie = buildRefreshCookie(refreshToken, {
431
+ maxAge: 86400,
432
+ sameSite: "Strict",
433
+ secure: true
434
+ });
435
+
436
+ res.setHeader("Set-Cookie", cookie);
437
+ ```
296
438
 
297
439
  ---
298
440
 
299
- ## 🔒 Security Best Practices
441
+ #### `buildClearRefreshCookie()`
442
+
443
+ Generates a cookie string to clear the refresh token (for logout).
444
+
445
+ **Returns:** `string`
446
+
447
+ **Example:**
448
+ ```javascript
449
+ app.post("/api/logout", (req, res) => {
450
+ res.setHeader("Set-Cookie", buildClearRefreshCookie());
451
+ res.json({ message: "Logged out" });
452
+ });
453
+ ```
454
+
455
+ ---
456
+
457
+ #### `deriveSecret(secret, userId, fpHash)`
458
+
459
+ Derives a unique signing secret using HMAC-SHA256 with the application's pepper.
460
+
461
+ **Parameters:**
462
+ - `secret` (string) - Base secret (access or refresh)
463
+ - `userId` (string) - User identifier
464
+ - `fpHash` (string) - Fingerprint hash
465
+
466
+ **Returns:** `string` (Hex-encoded derived secret)
467
+
468
+ **Note:** This is used internally. You typically don't need to call this directly.
469
+
470
+ ---
471
+
472
+ ## Security Best Practices
300
473
 
301
474
  ### 1. Environment Variables
302
475
 
303
476
  Never hardcode secrets. Use environment variables:
304
477
 
305
478
  ```bash
306
- # .env
307
- ACCESS_SECRET=your-super-secret-access-key-min-32-chars
308
- REFRESH_SECRET=your-super-secret-refresh-key-min-32-chars
479
+ # .env (add to .gitignore)
480
+ BRO_AUTH_SECRET_PEPPER=min-32-chars-random-string
481
+ ACCESS_SECRET=min-32-chars-random-string
482
+ REFRESH_SECRET=min-32-chars-random-string
483
+ ```
484
+
485
+ Generate secrets using:
486
+ ```bash
487
+ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
309
488
  ```
310
489
 
311
490
  ### 2. Token Storage
312
491
 
313
492
  **Access tokens:**
314
493
  - Store in memory (React state, Vue reactive)
315
- - Never in localStorage (XSS vulnerable)
494
+ - Session storage is acceptable for SPAs
495
+ - **Never** in localStorage (XSS vulnerable)
316
496
 
317
497
  **Refresh tokens:**
318
- - Use HTTP-only cookies (best)
319
- - Or secure memory storage with HTTPS
498
+ - HTTP-only, Secure, SameSite=Strict cookies (recommended)
499
+ - **Never** accessible to JavaScript
500
+
501
+ **Example:**
502
+ ```javascript
503
+ // ✓ Good: In-memory
504
+ const [accessToken, setAccessToken] = useState(null);
505
+
506
+ // ✗ Bad: localStorage
507
+ localStorage.setItem("token", accessToken); // XSS vulnerable
508
+ ```
320
509
 
321
510
  ### 3. HTTPS Only
322
511
 
323
- Always use HTTPS in production to prevent man-in-the-middle attacks.
512
+ Always use HTTPS in production:
324
513
 
325
- ### 4. Short-lived Access Tokens
514
+ ```javascript
515
+ const cookie = buildRefreshCookie(refreshToken, {
516
+ secure: process.env.NODE_ENV === "production",
517
+ sameSite: "Strict"
518
+ });
519
+ ```
326
520
 
327
- Keep access token TTL short (5-15 minutes) to limit exposure window.
521
+ ### 4. Short-Lived Access Tokens
522
+
523
+ Keep access token TTL short (5-15 minutes):
524
+
525
+ ```javascript
526
+ generateAccessToken(userId, fpHash, secret, "15m");
527
+ ```
328
528
 
329
529
  ### 5. Token Rotation
330
530
 
331
- Rotate refresh tokens on each use to detect token theft:
531
+ Rotate refresh tokens on each use:
332
532
 
333
533
  ```javascript
334
- // When refreshing, invalidate old refresh token
335
- // and issue a new pair
534
+ app.post("/api/refresh", async (req, res) => {
535
+ const result = verifyRefreshToken(/* ... */);
536
+
537
+ if (result.valid) {
538
+ // Issue new token pair
539
+ const newTokens = generateTokens(/* ... */);
540
+
541
+ // Optional: Invalidate old refresh token in database
542
+ await revokeToken(req.body.refreshToken);
543
+
544
+ res.json(newTokens);
545
+ }
546
+ });
336
547
  ```
337
548
 
338
- ---
549
+ ### 6. Rate Limiting
339
550
 
340
- ## 🎨 Integration Examples
551
+ Implement rate limiting on auth endpoints:
341
552
 
342
- ### Next.js App Router
553
+ ```javascript
554
+ import rateLimit from "express-rate-limit";
343
555
 
344
- ```typescript
345
- // app/api/login/route.ts
346
- import { generateTokens } from "bro-auth";
347
- import { NextRequest, NextResponse } from "next/server";
556
+ const loginLimiter = rateLimit({
557
+ windowMs: 15 * 60 * 1000,
558
+ max: 5
559
+ });
348
560
 
349
- export async function POST(req: NextRequest) {
350
- const { username, password, fingerprint } = await req.json();
351
-
352
- // Your auth logic
353
- const user = await authenticate(username, password);
354
-
355
- const tokens = generateTokens({
356
- userId: user.id,
357
- fingerprintHash: fingerprint,
358
- accessSecret: process.env.ACCESS_SECRET!,
359
- refreshSecret: process.env.REFRESH_SECRET!
360
- });
361
-
362
- return NextResponse.json(tokens);
363
- }
561
+ app.post("/api/login", loginLimiter, handleLogin);
364
562
  ```
365
563
 
564
+ ---
565
+
566
+ ## Framework Examples
567
+
366
568
  ### Express.js Middleware
367
569
 
368
570
  ```javascript
369
- import { verifyAccessToken } from "bro-auth";
571
+ import { verifyAccessToken } from "bro-auth/core";
370
572
 
371
573
  export const authMiddleware = (req, res, next) => {
372
574
  const token = req.headers.authorization?.replace("Bearer ", "");
373
575
  const fingerprint = req.headers["x-fingerprint"];
374
576
 
577
+ if (!token || !fingerprint) {
578
+ return res.status(401).json({ error: "Unauthorized" });
579
+ }
580
+
375
581
  const result = verifyAccessToken(
376
582
  token,
377
583
  fingerprint,
@@ -379,10 +585,10 @@ export const authMiddleware = (req, res, next) => {
379
585
  );
380
586
 
381
587
  if (!result.valid) {
382
- return res.status(401).json({ error: "Unauthorized" });
588
+ return res.status(401).json({ error: result.error });
383
589
  }
384
590
 
385
- req.userId = result.payload.userId;
591
+ req.userId = result.payload.sub;
386
592
  next();
387
593
  };
388
594
 
@@ -392,57 +598,225 @@ app.get("/api/user", authMiddleware, (req, res) => {
392
598
  });
393
599
  ```
394
600
 
395
- ---
601
+ ### Next.js 14 App Router
396
602
 
397
- ## ❓ FAQ
603
+ **Server Action:**
604
+ ```typescript
605
+ // app/actions/auth.ts
606
+ 'use server'
398
607
 
399
- ### Q: Can users have multiple devices?
608
+ import { generateTokens } from "bro-auth/core";
609
+ import { cookies } from "next/headers";
400
610
 
401
- **A:** Yes! Each device generates its own fingerprint. Issue separate token pairs for each device. You can track active sessions by storing fingerprint hashes (optional).
611
+ export async function loginAction(formData: FormData) {
612
+ const username = formData.get("username") as string;
613
+ const password = formData.get("password") as string;
614
+ const fingerprint = formData.get("fingerprint") as string;
615
+
616
+ const user = await authenticateUser(username, password);
617
+ if (!user) {
618
+ return { error: "Invalid credentials" };
619
+ }
620
+
621
+ const tokens = generateTokens(
622
+ user.id,
623
+ fingerprint,
624
+ process.env.ACCESS_SECRET!,
625
+ process.env.REFRESH_SECRET!
626
+ );
627
+
628
+ cookies().set("refreshToken", tokens.refreshToken, {
629
+ httpOnly: true,
630
+ secure: true,
631
+ sameSite: "strict",
632
+ maxAge: 60 * 60 * 24 * 7
633
+ });
634
+
635
+ return { accessToken: tokens.accessToken };
636
+ }
637
+ ```
402
638
 
403
- ### Q: What if fingerprint changes (browser update, etc.)?
639
+ **Client Component:**
640
+ ```typescript
641
+ // app/login/page.tsx
642
+ 'use client'
404
643
 
405
- **A:** The user will need to re-authenticate. This is a security feature—it prevents fingerprint spoofing. Consider implementing a "trusted devices" system for better UX.
644
+ import { getFingerprint } from "bro-auth/browser";
645
+ import { loginAction } from "@/app/actions/auth";
406
646
 
407
- ### Q: Is this more secure than sessions?
647
+ export default function LoginPage() {
648
+ async function handleSubmit(e: FormEvent) {
649
+ e.preventDefault();
650
+ const formData = new FormData(e.target as HTMLFormElement);
651
+
652
+ const fpHash = await getFingerprint();
653
+ formData.append("fingerprint", fpHash);
654
+
655
+ const result = await loginAction(formData);
656
+ if (result.accessToken) {
657
+ sessionStorage.setItem("accessToken", result.accessToken);
658
+ router.push("/dashboard");
659
+ }
660
+ }
661
+
662
+ return (
663
+ <form onSubmit={handleSubmit}>
664
+ <input name="username" type="email" required />
665
+ <input name="password" type="password" required />
666
+ <button type="submit">Login</button>
667
+ </form>
668
+ );
669
+ }
670
+ ```
408
671
 
409
- **A:** It's different. Sessions require server-side storage but can be invalidated instantly. bro-auth is stateless (scales better) but tokens can't be revoked until expiry. Choose based on your needs.
672
+ ### React Auth Context
410
673
 
411
- ### Q: What about privacy?
674
+ ```javascript
675
+ import { createContext, useContext, useState, useEffect } from "react";
676
+ import { getFingerprint } from "bro-auth/browser";
412
677
 
413
- **A:** The fingerprint is hashed with SHA-256 before storage. Only the hash is sent to the server—individual components stay client-side. However, browser fingerprinting can be privacy-sensitive, so disclose this in your privacy policy.
678
+ const AuthContext = createContext();
414
679
 
415
- ### Q: Does this work with mobile apps?
680
+ export function AuthProvider({ children }) {
681
+ const [accessToken, setAccessToken] = useState(null);
682
+ const [fingerprint, setFingerprint] = useState(null);
683
+
684
+ useEffect(() => {
685
+ getFingerprint().then(setFingerprint);
686
+ }, []);
687
+
688
+ async function login(username, password) {
689
+ const response = await fetch("/api/login", {
690
+ method: "POST",
691
+ headers: { "Content-Type": "application/json" },
692
+ body: JSON.stringify({ username, password, fingerprint })
693
+ });
694
+
695
+ const data = await response.json();
696
+ setAccessToken(data.accessToken);
697
+ }
698
+
699
+ async function apiCall(endpoint, options = {}) {
700
+ return fetch(endpoint, {
701
+ ...options,
702
+ headers: {
703
+ ...options.headers,
704
+ "Authorization": `Bearer ${accessToken}`,
705
+ "X-Fingerprint": fingerprint
706
+ }
707
+ });
708
+ }
709
+
710
+ return (
711
+ <AuthContext.Provider value={{ login, apiCall, accessToken }}>
712
+ {children}
713
+ </AuthContext.Provider>
714
+ );
715
+ }
416
716
 
417
- **A:** The browser module is web-only. For mobile apps, generate a device ID using native APIs (iOS: `identifierForVendor`, Android: `ANDROID_ID`) and use that as the fingerprint.
717
+ export const useAuth = () => useContext(AuthContext);
718
+ ```
418
719
 
419
720
  ---
420
721
 
421
- ## 🤝 Contributing
722
+ ## FAQ
422
723
 
423
- Contributions are welcome! Please feel free to submit a Pull Request.
724
+ **Q: Can users have multiple devices?**
424
725
 
425
- 1. Fork the repository
426
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
427
- 3. Commit your changes (`git commit -m 'Add amazing feature'`)
428
- 4. Push to the branch (`git push origin feature/amazing-feature`)
429
- 5. Open a Pull Request
726
+ A: Yes. Each device generates its own fingerprint. Issue separate token pairs for each device. Optionally track active sessions by storing fingerprint hashes.
727
+
728
+ **Q: What if the fingerprint changes (browser update)?**
729
+
730
+ A: The user must re-authenticate. This is intentional—it prevents fingerprint spoofing. For better UX, implement a "trusted devices" feature or send email notifications for new logins.
731
+
732
+ **Q: What about privacy concerns?**
733
+
734
+ A: The fingerprint is SHA-256 hashed before transmission. Only the hash is sent to the server. However, disclose fingerprinting in your privacy policy and comply with GDPR/CCPA.
735
+
736
+ **Q: Does this work with mobile apps?**
737
+
738
+ A: The browser module is web-only. For mobile apps, use native device identifiers:
739
+ - iOS: `identifierForVendor`
740
+ - Android: `ANDROID_ID`
741
+ - React Native: `react-native-device-info`
742
+
743
+ **Q: How do I invalidate tokens immediately?**
744
+
745
+ A: `bro-auth` is stateless, so tokens can't be revoked until expiry. For immediate invalidation:
746
+ - Maintain a token blacklist in Redis
747
+ - Use short-lived access tokens (5-15 min)
748
+ - Implement token versioning (increment on password change)
749
+
750
+ **Q: Can I use this with GraphQL?**
751
+
752
+ A: Yes. Pass credentials in HTTP headers:
753
+
754
+ ```javascript
755
+ const client = new ApolloClient({
756
+ uri: '/graphql',
757
+ request: async (operation) => {
758
+ const fpHash = await getFingerprint();
759
+ operation.setContext({
760
+ headers: {
761
+ authorization: `Bearer ${accessToken}`,
762
+ 'x-fingerprint': fpHash
763
+ }
764
+ });
765
+ }
766
+ });
767
+ ```
430
768
 
431
769
  ---
432
770
 
433
- ## 📄 License
771
+ ## Testing
434
772
 
435
- MIT © Vaishnav
773
+ ### Run Backend Tests
774
+
775
+ ```bash
776
+ npm test
777
+ ```
778
+
779
+ Tests:
780
+ - Token generation
781
+ - Token verification
782
+ - Fingerprint binding
783
+ - Secret derivation
784
+ - Invalid fingerprint rejection
785
+
786
+ ### Run Browser Tests
787
+
788
+ ```bash
789
+ npx serve .
790
+ ```
791
+
792
+ Open: `http://localhost:3000/tests/test-browser.html`
793
+
794
+ Tests:
795
+ - Fingerprint generation
796
+ - SHA-256 hashing
797
+ - Browser compatibility
436
798
 
437
799
  ---
438
800
 
801
+ ## Contributing
802
+
803
+ Contributions welcome. Please:
804
+
805
+ 1. Fork the repository
806
+ 2. Create a feature branch
807
+ 3. Run tests (`npm test`)
808
+ 4. Submit a pull request
809
+
439
810
  ---
440
811
 
441
- ## 🔗 Links
812
+ ## License
442
813
 
443
- - [NPM Package](https://www.npmjs.com/package/bro-auth)
444
- - [GitHub Repository](https://github.com/ChakraVaishnav/bro-auth)
814
+ MIT © Vaishnav
445
815
 
446
816
  ---
447
817
 
448
- **Made with 💪 by developers who care about security**
818
+ ## Links
819
+
820
+ - [NPM Package](https://www.npmjs.com/package/bro-auth)
821
+ - [GitHub Repository](https://github.com/ChakraVaishnav/bro-auth)
822
+ - [Report Issues](https://github.com/ChakraVaishnav/bro-auth/issues)