secure-web-token 1.2.10 → 1.2.12
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 +290 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,47 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://res.cloudinary.com/dch9wfmjd/image/upload/v1778127677/varient-1-circle_wykez9.png" alt="Secure Web Token Logo" width="48" align="center" />
|
|
3
|
+
<strong>secure-web-token (SWT)</strong>
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<strong>The secure, encrypted, device-bound alternative to JWT — built for Node.js</strong>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<img src="https://res.cloudinary.com/dch9wfmjd/image/upload/v1778126974/downloads-badge_vyp6px.svg" alt="50K+ Downloads" width="560" />
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/secure-web-token">
|
|
16
|
+
<img src="https://img.shields.io/badge/downloads-50k%2B-orange?logo=npm" alt="Downloads" />
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://github.com/MintuSingh07/node-securewebtoken/stargazers">
|
|
19
|
+
<img src="https://img.shields.io/github/stars/MintuSingh07/node-securewebtoken?style=flat&logo=github&color=yellow" alt="GitHub Stars" />
|
|
20
|
+
</a>
|
|
21
|
+
<a href="https://www.npmjs.com/package/secure-web-token">
|
|
22
|
+
<img src="https://img.shields.io/badge/node-%3E%3D25.5.0-green?logo=node.js" alt="Node.js Version" />
|
|
23
|
+
</a>
|
|
24
|
+
<a href="https://www.npmjs.com/package/secure-web-token">
|
|
25
|
+
<img src="https://img.shields.io/badge/TypeScript-Ready-3178C6?logo=typescript" alt="TypeScript Ready" />
|
|
26
|
+
</a>
|
|
27
|
+
<a href="https://github.com/MintuSingh07/node-securewebtoken">
|
|
28
|
+
<img src="https://img.shields.io/badge/Encryption-AES--256--GCM-brightgreen" alt="AES-256-GCM" />
|
|
29
|
+
</a>
|
|
30
|
+
<a href="https://github.com/MintuSingh07/node-securewebtoken/pulls">
|
|
31
|
+
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs Welcome" />
|
|
32
|
+
</a>
|
|
33
|
+
<a href="https://snyk.io/test/github/MintuSingh07/node-securewebtoken">
|
|
34
|
+
<img src="https://snyk.io/test/github/MintuSingh07/node-securewebtoken/badge.svg" alt="Known Vulnerabilities" />
|
|
35
|
+
</a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<a href="#why-swt">Why SWT?</a> •
|
|
40
|
+
<a href="#installation">Installation</a> •
|
|
41
|
+
<a href="#quick-start">Quick Start</a> •
|
|
42
|
+
<a href="#full-expressjs-example">Full Example</a> •
|
|
43
|
+
<a href="#swt-vs-jwt--deep-comparison">SWT vs JWT</a> •
|
|
44
|
+
<a href="#faq">FAQ</a> •
|
|
45
|
+
<a href="#roadmap">Roadmap</a>
|
|
46
|
+
</p>
|
|
16
47
|
|
|
17
48
|
---
|
|
18
49
|
|
|
19
|
-
## Why SWT?
|
|
50
|
+
## Why SWT?
|
|
20
51
|
|
|
21
|
-
**JWT has well-known, unfixed security
|
|
52
|
+
**JWT has well-known, unfixed security problems.** If you're running a security-critical app — admin panel, SaaS dashboard, fintech, healthcare — and you haven't thought about these, stop and read this.
|
|
22
53
|
|
|
23
|
-
| Problem
|
|
24
|
-
|
|
25
|
-
| Payload encryption
|
|
26
|
-
| Device binding
|
|
27
|
-
| True logout
|
|
28
|
-
| Token theft impact
|
|
29
|
-
| Sensitive data in token | ❌ Visible in browser devtools
|
|
54
|
+
| Problem | JWT | SWT |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Payload encryption | ❌ Base64 only — readable by anyone | ✅ AES-256-GCM encrypted |
|
|
57
|
+
| Device binding | ❌ Token works on any device, anywhere | ✅ Bound to the original device/session |
|
|
58
|
+
| True logout | ❌ Tokens stay valid after logout | ✅ Instant server-side revocation |
|
|
59
|
+
| Token theft impact | ❌ Stolen token = full account access | ✅ Stolen token is useless on another device |
|
|
60
|
+
| Sensitive data in token | ❌ Visible in browser devtools | ✅ Encrypted, never exposed |
|
|
30
61
|
|
|
31
|
-
> **If you
|
|
62
|
+
> **If you're storing user roles, permissions, or any sensitive identifiers in a JWT — they're readable by anyone who gets that token.** SWT fixes this at the architecture level.
|
|
32
63
|
|
|
33
64
|
---
|
|
34
65
|
|
|
35
|
-
## What is Secure Web Token
|
|
66
|
+
## What is Secure Web Token?
|
|
67
|
+
|
|
68
|
+
**Secure Web Token (SWT)** is a Node.js authentication library that replaces JWT with a system that is fundamentally more secure by design. It solves all four of JWT's critical weaknesses in one package.
|
|
36
69
|
|
|
37
|
-
**
|
|
70
|
+
**How it works:**
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
72
|
+
- 🔐 **AES-256-GCM Encryption** — Your token payload is fully encrypted, not just Base64 encoded. No one can read it without the server secret.
|
|
73
|
+
- 📱 **Device Binding** — Each token is tied to the exact device it was issued to via a server-stored fingerprint. A stolen token cannot be replayed from a different device.
|
|
74
|
+
- 🗄️ **Server-Side Session Management** — Sessions live on the server. Logout actually works — revocation is instant and permanent.
|
|
75
|
+
- 🍪 **HttpOnly Cookie + Token Dual Guard** — The session ID lives in an HttpOnly cookie (XSS-proof), the encrypted payload travels via Authorization header. Neither alone is enough.
|
|
43
76
|
|
|
44
|
-
**Best suited for:** Admin
|
|
77
|
+
**Best suited for:** Admin panels, SaaS dashboards, course platforms, internal tools, healthcare apps, fintech APIs, and any application where a stolen session is unacceptable.
|
|
45
78
|
|
|
46
79
|
---
|
|
47
80
|
|
|
@@ -70,13 +103,18 @@ import { sign } from "secure-web-token";
|
|
|
70
103
|
|
|
71
104
|
const SECRET = "your-256-bit-secret";
|
|
72
105
|
|
|
73
|
-
const { token, sessionId } = sign(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
const { token, sessionId } = sign(
|
|
107
|
+
{ userId: 1, role: "admin" },
|
|
108
|
+
SECRET,
|
|
109
|
+
{
|
|
110
|
+
fingerprint: true, // bind to this device
|
|
111
|
+
store: "memory", // server-side session store
|
|
112
|
+
expiresIn: 3600, // expires in 1 hour
|
|
113
|
+
}
|
|
114
|
+
);
|
|
78
115
|
|
|
79
|
-
// Send `token` to client
|
|
116
|
+
// → Send `token` to client
|
|
117
|
+
// → Store `sessionId` in an HttpOnly cookie (never send to client directly)
|
|
80
118
|
```
|
|
81
119
|
|
|
82
120
|
### 2. Verify a Token (Protected Route)
|
|
@@ -85,15 +123,23 @@ const { token, sessionId } = sign({ userId: 1, role: "admin" }, SECRET, {
|
|
|
85
123
|
import { verify, getStore } from "secure-web-token";
|
|
86
124
|
|
|
87
125
|
const store = getStore("memory");
|
|
88
|
-
const session = store.getSession(sessionId); // from HttpOnly cookie
|
|
126
|
+
const session = store.getSession(sessionId); // retrieved from HttpOnly cookie
|
|
89
127
|
|
|
90
128
|
const payload = verify(token, SECRET, {
|
|
91
129
|
sessionId,
|
|
92
|
-
fingerprint: session.fingerprint,
|
|
130
|
+
fingerprint: session.fingerprint, // must match original device
|
|
93
131
|
store: "memory",
|
|
94
132
|
});
|
|
95
133
|
|
|
96
|
-
// payload.data
|
|
134
|
+
// payload.data → { userId: 1, role: "admin" }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 3. Logout (True Revocation)
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
// Session is deleted server-side — token is immediately dead
|
|
141
|
+
store.deleteSession(sessionId);
|
|
142
|
+
res.clearCookie("swt_session");
|
|
97
143
|
```
|
|
98
144
|
|
|
99
145
|
---
|
|
@@ -103,17 +149,22 @@ const payload = verify(token, SECRET, {
|
|
|
103
149
|
```ts
|
|
104
150
|
import express from "express";
|
|
105
151
|
import cookieParser from "cookie-parser";
|
|
152
|
+
import cors from "cors";
|
|
106
153
|
import { sign, verify, getStore } from "secure-web-token";
|
|
107
154
|
|
|
108
155
|
const app = express();
|
|
109
|
-
app.use(
|
|
156
|
+
app.use(cors({ origin: true, credentials: true }));
|
|
110
157
|
app.use(cookieParser());
|
|
158
|
+
app.use(express.json());
|
|
111
159
|
|
|
112
160
|
const SECRET = process.env.SWT_SECRET!;
|
|
113
161
|
const store = getStore("memory");
|
|
114
162
|
|
|
115
|
-
//
|
|
163
|
+
// ──────────────────────────────────────────
|
|
164
|
+
// POST /login — Issue a secure session
|
|
165
|
+
// ──────────────────────────────────────────
|
|
116
166
|
app.post("/login", (req, res) => {
|
|
167
|
+
// Authenticate user here (DB lookup, password check, etc.)
|
|
117
168
|
const user = { userId: 1, name: "Alice", role: "admin" };
|
|
118
169
|
|
|
119
170
|
const { token, sessionId } = sign(user, SECRET, {
|
|
@@ -122,14 +173,20 @@ app.post("/login", (req, res) => {
|
|
|
122
173
|
expiresIn: 3600,
|
|
123
174
|
});
|
|
124
175
|
|
|
125
|
-
// sessionId
|
|
126
|
-
res.cookie("swt_session", sessionId, {
|
|
176
|
+
// sessionId → HttpOnly cookie (invisible to JavaScript, XSS-proof)
|
|
177
|
+
res.cookie("swt_session", sessionId, {
|
|
178
|
+
httpOnly: true,
|
|
179
|
+
secure: true,
|
|
180
|
+
sameSite: "strict",
|
|
181
|
+
});
|
|
127
182
|
|
|
128
|
-
// Encrypted token
|
|
183
|
+
// Encrypted token → client (localStorage or memory)
|
|
129
184
|
res.json({ token });
|
|
130
185
|
});
|
|
131
186
|
|
|
132
|
-
//
|
|
187
|
+
// ──────────────────────────────────────────
|
|
188
|
+
// GET /profile — Protected route
|
|
189
|
+
// ──────────────────────────────────────────
|
|
133
190
|
app.get("/profile", (req, res) => {
|
|
134
191
|
try {
|
|
135
192
|
const sessionId = req.cookies.swt_session;
|
|
@@ -148,10 +205,12 @@ app.get("/profile", (req, res) => {
|
|
|
148
205
|
}
|
|
149
206
|
});
|
|
150
207
|
|
|
151
|
-
//
|
|
208
|
+
// ──────────────────────────────────────────
|
|
209
|
+
// POST /logout — True session revocation
|
|
210
|
+
// ──────────────────────────────────────────
|
|
152
211
|
app.post("/logout", (req, res) => {
|
|
153
212
|
const sessionId = req.cookies.swt_session;
|
|
154
|
-
store.deleteSession(sessionId);
|
|
213
|
+
store.deleteSession(sessionId); // token is dead immediately
|
|
155
214
|
res.clearCookie("swt_session");
|
|
156
215
|
res.json({ success: true });
|
|
157
216
|
});
|
|
@@ -159,11 +218,60 @@ app.post("/logout", (req, res) => {
|
|
|
159
218
|
app.listen(4000);
|
|
160
219
|
```
|
|
161
220
|
|
|
221
|
+
### Frontend (React)
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { useState } from "react";
|
|
225
|
+
|
|
226
|
+
function App() {
|
|
227
|
+
const [user, setUser] = useState(null);
|
|
228
|
+
|
|
229
|
+
const login = async () => {
|
|
230
|
+
const res = await fetch("http://localhost:4000/login", {
|
|
231
|
+
method: "POST",
|
|
232
|
+
credentials: "include", // sends/receives the HttpOnly cookie
|
|
233
|
+
});
|
|
234
|
+
const { token } = await res.json();
|
|
235
|
+
localStorage.setItem("swt_token", token);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const getProfile = async () => {
|
|
239
|
+
const token = localStorage.getItem("swt_token");
|
|
240
|
+
const res = await fetch("http://localhost:4000/profile", {
|
|
241
|
+
credentials: "include",
|
|
242
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
243
|
+
});
|
|
244
|
+
const data = await res.json();
|
|
245
|
+
setUser(data.user);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const logout = async () => {
|
|
249
|
+
await fetch("http://localhost:4000/logout", {
|
|
250
|
+
method: "POST",
|
|
251
|
+
credentials: "include",
|
|
252
|
+
});
|
|
253
|
+
localStorage.removeItem("swt_token");
|
|
254
|
+
setUser(null);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<>
|
|
259
|
+
<button onClick={login}>Login</button>
|
|
260
|
+
<button onClick={getProfile}>View Profile</button>
|
|
261
|
+
<button onClick={logout}>Logout</button>
|
|
262
|
+
{user && <pre>{JSON.stringify(user, null, 2)}</pre>}
|
|
263
|
+
</>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export default App;
|
|
268
|
+
```
|
|
269
|
+
|
|
162
270
|
---
|
|
163
271
|
|
|
164
272
|
## Token Payload Structure
|
|
165
273
|
|
|
166
|
-
The payload
|
|
274
|
+
The payload delivered to the client is **fully AES-256-GCM encrypted**. What lives inside (server-side only):
|
|
167
275
|
|
|
168
276
|
```json
|
|
169
277
|
{
|
|
@@ -177,88 +285,145 @@ The payload sent to the client is **fully AES-256-GCM encrypted**. Internally it
|
|
|
177
285
|
}
|
|
178
286
|
```
|
|
179
287
|
|
|
180
|
-
Unlike JWT,
|
|
288
|
+
Unlike JWT, this structure **cannot be decoded in the browser**. There is no `atob()` trick. Without the server secret, it is ciphertext.
|
|
181
289
|
|
|
182
290
|
---
|
|
183
291
|
|
|
184
292
|
## SWT vs JWT — Deep Comparison
|
|
185
293
|
|
|
186
|
-
###
|
|
294
|
+
### The 4 Security Problems with JWT
|
|
295
|
+
|
|
296
|
+
**1. Payloads are not encrypted**
|
|
297
|
+
|
|
298
|
+
JWT uses Base64URL encoding — not encryption. Anyone with the token can decode the payload instantly:
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
// This works on ANY JWT right now — no key required
|
|
302
|
+
JSON.parse(atob(token.split('.')[1]));
|
|
303
|
+
// → { userId: 1, role: "admin", email: "alice@example.com" }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
If your JWT payload leaks (XSS, logs, network interception), all your user data is exposed in plaintext.
|
|
307
|
+
|
|
308
|
+
**2. No device binding**
|
|
309
|
+
|
|
310
|
+
A JWT issued in one country works equally from any other device or server. There is no native way to say "this token belongs to this device." A stolen token is a valid credential — period.
|
|
187
311
|
|
|
188
|
-
**
|
|
189
|
-
JWT payloads are Base64URL encoded — not encrypted. Anyone who intercepts or steals the token can read the payload. If you store `role: "admin"` in a JWT, an attacker can see it.
|
|
312
|
+
**3. Logout is not real**
|
|
190
313
|
|
|
191
|
-
|
|
192
|
-
A JWT issued to a user in New York can be used from a server in Russia. There is no native mechanism in JWT to prevent this.
|
|
314
|
+
JWT is stateless by design. Once issued, a token remains cryptographically valid until it expires — regardless of what you do on the server. Client-side logout (clearing cookies/localStorage) doesn't invalidate the token. An attacker who stole it before logout still has access.
|
|
193
315
|
|
|
194
|
-
**
|
|
195
|
-
JWT is stateless. Once issued, a JWT is valid until it expires — even after the user logs out. The only fix (token blocklist) defeats the purpose of being stateless.
|
|
316
|
+
**4. Token theft = full session compromise**
|
|
196
317
|
|
|
197
|
-
|
|
198
|
-
If a JWT is stolen via XSS or network interception, the attacker has full access for the token's entire lifetime.
|
|
318
|
+
There is no fallback. A stolen JWT gives the attacker the same access as the legitimate user for the token's entire lifetime, with no way to tell them apart.
|
|
199
319
|
|
|
200
|
-
### How SWT Fixes
|
|
320
|
+
### How SWT Fixes All Four
|
|
201
321
|
|
|
202
|
-
| JWT Flaw
|
|
203
|
-
|
|
204
|
-
| Readable payload
|
|
205
|
-
| No device binding
|
|
206
|
-
| Logout doesn't work |
|
|
207
|
-
| Token theft
|
|
322
|
+
| JWT Flaw | SWT Solution |
|
|
323
|
+
|---|---|
|
|
324
|
+
| Readable payload | AES-256-GCM — unreadable without the server secret |
|
|
325
|
+
| No device binding | Device fingerprint stored in server session — wrong device = rejected |
|
|
326
|
+
| Logout doesn't work | `store.deleteSession()` — immediate, permanent revocation |
|
|
327
|
+
| Token theft | Stolen token fails fingerprint check on any other device |
|
|
328
|
+
|
|
329
|
+
### Attack Surface Comparison
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
JWT Attack Model:
|
|
333
|
+
Attacker steals token via XSS
|
|
334
|
+
→ Token is valid anywhere
|
|
335
|
+
→ Full account access until expiry
|
|
336
|
+
→ Nothing you can do
|
|
337
|
+
|
|
338
|
+
SWT Attack Model:
|
|
339
|
+
Attacker steals token via XSS
|
|
340
|
+
→ Token requires matching HttpOnly cookie (not stealable via XSS)
|
|
341
|
+
→ Even with both, device fingerprint must match
|
|
342
|
+
→ Session can be revoked server-side instantly
|
|
343
|
+
```
|
|
208
344
|
|
|
209
345
|
---
|
|
210
346
|
|
|
211
|
-
##
|
|
347
|
+
## Security Architecture
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
Client Server
|
|
351
|
+
│ │
|
|
352
|
+
│ POST /login │
|
|
353
|
+
├──────────────────────────────────►│
|
|
354
|
+
│ │ sign(payload, secret, { fingerprint: true })
|
|
355
|
+
│ │ ┌───────────────────────────────────┐
|
|
356
|
+
│ │ │ 1. Encrypt payload (AES-256-GCM) │
|
|
357
|
+
│ │ │ 2. Generate device fingerprint │
|
|
358
|
+
│ │ │ 3. Store session server-side │
|
|
359
|
+
│ │ └───────────────────────────────────┘
|
|
360
|
+
│ { token } + Cookie: sessionId │
|
|
361
|
+
│◄──────────────────────────────────┤
|
|
362
|
+
│ │
|
|
363
|
+
│ GET /profile │
|
|
364
|
+
│ Authorization: Bearer <token> │
|
|
365
|
+
│ Cookie: swt_session=<id> │
|
|
366
|
+
├──────────────────────────────────►│
|
|
367
|
+
│ │ verify(token, secret, { sessionId, fingerprint })
|
|
368
|
+
│ │ ┌───────────────────────────────────┐
|
|
369
|
+
│ │ │ 1. Decrypt token │
|
|
370
|
+
│ │ │ 2. Match device fingerprint │
|
|
371
|
+
│ │ │ 3. Validate active server session │
|
|
372
|
+
│ │ └───────────────────────────────────┘
|
|
373
|
+
│ { user: { ... } } │
|
|
374
|
+
│◄──────────────────────────────────┤
|
|
375
|
+
│ │
|
|
376
|
+
│ POST /logout │
|
|
377
|
+
├──────────────────────────────────►│
|
|
378
|
+
│ │ store.deleteSession(sessionId)
|
|
379
|
+
│ │ → Token is dead. Immediately.
|
|
380
|
+
│ { success: true } │
|
|
381
|
+
│◄──────────────────────────────────┤
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## FAQ
|
|
212
387
|
|
|
213
388
|
**Q: Is SWT a drop-in replacement for JWT?**
|
|
214
|
-
A: The API is simple and migration is straightforward. Instead of `jwt.sign()` use `swt.sign()`. The main addition is server-side session storage and device fingerprinting.
|
|
215
389
|
|
|
216
|
-
|
|
217
|
-
|
|
390
|
+
Migration is straightforward. Replace `jwt.sign()` with `sign()` from SWT and `jwt.verify()` with `verify()`. The main additions are server-side session storage and device fingerprinting — both handled automatically when you pass `fingerprint: true`.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
**Q: What encryption algorithm does SWT use?**
|
|
395
|
+
|
|
396
|
+
AES-256-GCM — the gold standard for symmetric authenticated encryption, recommended by NIST, and the same cipher used in TLS 1.3. It provides both confidentiality and integrity (tamper detection) in a single pass.
|
|
397
|
+
|
|
398
|
+
---
|
|
218
399
|
|
|
219
|
-
**Q: Does SWT support Redis?**
|
|
220
|
-
A: The architecture is Redis-ready. The store interface is designed to plug in Redis for production distributed systems.
|
|
400
|
+
**Q: Does SWT support Redis for distributed systems?**
|
|
221
401
|
|
|
222
|
-
|
|
223
|
-
|
|
402
|
+
The architecture is Redis-ready by design. The session store interface is built to accept pluggable adapters — Redis support is on the roadmap and can be integrated without changing your application code.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
**Q: SWT is stateful. Isn't stateless better?**
|
|
407
|
+
|
|
408
|
+
Stateless JWT trades security for scalability. That tradeoff made sense for internal microservices, but not for user-facing auth. SWT uses a minimal server-side footprint — one small session record per active user — which is manageable at any production scale. The security gains far outweigh the overhead.
|
|
409
|
+
|
|
410
|
+
---
|
|
224
411
|
|
|
225
412
|
**Q: When should I still use JWT?**
|
|
226
|
-
|
|
413
|
+
|
|
414
|
+
JWT is fine for short-lived, low-sensitivity tokens between internal services where interception risk is low and logout/device binding don't matter. For any user-facing session, SWT is the better choice.
|
|
415
|
+
|
|
416
|
+
---
|
|
227
417
|
|
|
228
418
|
**Q: What Node.js version is required?**
|
|
229
|
-
|
|
419
|
+
|
|
420
|
+
Node.js `>=25.5.0`. SWT uses the native `crypto` module for AES-256-GCM — no external cryptography dependencies.
|
|
230
421
|
|
|
231
422
|
---
|
|
232
423
|
|
|
233
|
-
|
|
424
|
+
**Q: Does SWT prevent XSS attacks entirely?**
|
|
234
425
|
|
|
235
|
-
|
|
236
|
-
Client Server
|
|
237
|
-
│ │
|
|
238
|
-
│ POST /login │
|
|
239
|
-
├──────────────────────────────►│
|
|
240
|
-
│ │ sign(payload, secret, { fingerprint: true })
|
|
241
|
-
│ │ ┌─────────────────────────────────┐
|
|
242
|
-
│ │ │ 1. Encrypt payload (AES-256-GCM)│
|
|
243
|
-
│ │ │ 2. Generate device fingerprint │
|
|
244
|
-
│ │ │ 3. Store session server-side │
|
|
245
|
-
│ │ └─────────────────────────────────┘
|
|
246
|
-
│ { token } + [HttpOnly Cookie: sessionId]
|
|
247
|
-
│◄──────────────────────────────┤
|
|
248
|
-
│ │
|
|
249
|
-
│ GET /profile │
|
|
250
|
-
│ Authorization: Bearer <token>│
|
|
251
|
-
│ Cookie: swt_session=<id> │
|
|
252
|
-
├──────────────────────────────►│
|
|
253
|
-
│ │ verify(token, secret, { sessionId, fingerprint })
|
|
254
|
-
│ │ ┌─────────────────────────────────┐
|
|
255
|
-
│ │ │ 1. Decrypt token │
|
|
256
|
-
│ │ │ 2. Match device fingerprint │
|
|
257
|
-
│ │ │ 3. Validate server session │
|
|
258
|
-
│ │ └─────────────────────────────────┘
|
|
259
|
-
│ { user: { ... } } │
|
|
260
|
-
│◄──────────────────────────────┤
|
|
261
|
-
```
|
|
426
|
+
SWT significantly reduces the impact of XSS. Because the session ID lives in an HttpOnly cookie, XSS cannot steal it via `document.cookie`. An attacker who steals only the bearer token still can't authenticate without the cookie — and even if they somehow get both, the device fingerprint check provides a third layer of validation.
|
|
262
427
|
|
|
263
428
|
---
|
|
264
429
|
|
|
@@ -266,27 +431,39 @@ Client Server
|
|
|
266
431
|
|
|
267
432
|
- [x] AES-256-GCM payload encryption
|
|
268
433
|
- [x] Device fingerprint binding
|
|
269
|
-
- [x]
|
|
434
|
+
- [x] In-memory session store
|
|
270
435
|
- [x] Token expiry (`iat`, `exp`)
|
|
271
436
|
- [ ] Redis session store adapter
|
|
272
|
-
- [ ] Token rotation / refresh
|
|
273
|
-
- [ ] TypeScript types
|
|
274
|
-
- [ ] Express.js middleware helper
|
|
437
|
+
- [ ] Token rotation / silent refresh
|
|
438
|
+
- [ ] Strict TypeScript types
|
|
439
|
+
- [ ] Express.js middleware helper (`swtMiddleware()`)
|
|
275
440
|
- [ ] Audit log support
|
|
441
|
+
- [ ] React hooks (`useSWT`)
|
|
276
442
|
|
|
277
443
|
---
|
|
278
444
|
|
|
279
445
|
## Contributing
|
|
280
446
|
|
|
281
|
-
PRs and issues welcome.
|
|
447
|
+
PRs and issues are welcome. For security vulnerabilities, please open a **private security advisory** on GitHub rather than a public issue.
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
git clone https://github.com/MintuSingh07/node-securewebtoken.git
|
|
451
|
+
cd node-securewebtoken
|
|
452
|
+
npm install
|
|
453
|
+
npm run build
|
|
454
|
+
```
|
|
282
455
|
|
|
283
456
|
---
|
|
284
457
|
|
|
285
458
|
## License
|
|
286
459
|
|
|
287
|
-
MIT
|
|
460
|
+
[MIT](./LICENSE) © [MintuSingh07](https://github.com/MintuSingh07)
|
|
288
461
|
|
|
289
462
|
---
|
|
290
463
|
|
|
291
|
-
>
|
|
292
|
-
>
|
|
464
|
+
<p align="center">
|
|
465
|
+
<strong>Stop using JWT for sensitive user sessions.</strong><br/>
|
|
466
|
+
Your users deserve encrypted, device-bound, truly revocable auth.
|
|
467
|
+
<br/><br/>
|
|
468
|
+
<code>npm install secure-web-token</code>
|
|
469
|
+
</p>
|