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