secure-web-token 1.2.7 โ 1.2.9
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 +175 -107
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,131 +1,135 @@
|
|
|
1
|
-
#
|
|
1
|
+
# secure-web-token (SWT)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **The secure alternative to JWT** โ encrypted, device-bound, and built for production security.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/secure-web-token)
|
|
6
|
+
[](https://www.npmjs.com/package/secure-web-token)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install secure-web-token
|
|
11
|
+
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
---
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## Why SWT? (The JWT Problem)
|
|
14
16
|
|
|
15
|
-
-
|
|
16
|
-
- ๐งท **Device-bound tokens (single-device login)**
|
|
17
|
-
- ๐ **Server-side session management**
|
|
18
|
-
- ๐ช **HttpOnly session cookies**
|
|
19
|
-
- โฑ **Expiry support (`iat`, `exp`)**
|
|
20
|
-
- โก Simple API: `sign()` and `verify()`
|
|
21
|
-
- ๐ง Memory store (Redis-ready design)
|
|
17
|
+
**JWT has well-known, unfixed security flaws.** If you are using JWT in a security-critical app and have not thought about these, you should stop and read this:
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
| Problem | JWT | SWT (Secure Web Token) |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Payload encryption | โ Base64 only (readable by anyone) | โ
AES-256-GCM encrypted |
|
|
22
|
+
| Device binding | โ Token works on any device | โ
Bound to one device/session |
|
|
23
|
+
| True logout | โ Tokens stay valid after logout | โ
Server-side revocation |
|
|
24
|
+
| Token theft impact | โ Stolen token = full account access | โ
Stolen token is useless on another device |
|
|
25
|
+
| Sensitive data in token | โ Visible in browser devtools | โ
Encrypted, never exposed |
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
> **If you are storing user roles, permissions, or any sensitive identifiers in a JWT โ they are readable by anyone with the token.** SWT fixes this.
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
---
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
- Tokens can be reused on any device
|
|
31
|
-
- No native device binding
|
|
32
|
-
- Logout does not truly invalidate tokens
|
|
31
|
+
## What is Secure Web Token (SWT)?
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
**Secure Web Token (SWT)** is a Node.js authentication library that replaces JWT with a system that is fundamentally more secure by design:
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
- Sensitive identifiers never reach the browser
|
|
35
|
+
1. **AES-256-GCM Encryption** โ Your token payload is fully encrypted, not just encoded.
|
|
36
|
+
2. **Device Binding** โ Each token is tied to the exact device it was issued to. A stolen token cannot be used from a different device.
|
|
37
|
+
3. **Server-Side Session Management** โ Sessions live on the server. Logout actually works.
|
|
38
|
+
4. **HttpOnly Cookie + Token Dual Guard** โ Combines the security of HttpOnly cookies with encrypted bearer tokens.
|
|
41
39
|
|
|
42
|
-
**Best suited for:**
|
|
43
|
-
- Admin panels
|
|
44
|
-
- SaaS dashboards
|
|
45
|
-
- Course platforms
|
|
46
|
-
- Internal tools
|
|
47
|
-
- High-security APIs
|
|
40
|
+
**Best suited for:** Admin dashboards, SaaS apps, course platforms, internal tools, healthcare apps, fintech, and any application where a stolen session is unacceptable.
|
|
48
41
|
|
|
49
42
|
---
|
|
50
43
|
|
|
51
|
-
##
|
|
44
|
+
## Installation
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
```bash
|
|
47
|
+
npm install secure-web-token
|
|
48
|
+
```
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
```ts
|
|
51
|
+
// ESM
|
|
52
|
+
import { sign, verify, getStore } from "secure-web-token";
|
|
58
53
|
|
|
59
|
-
|
|
54
|
+
// CommonJS
|
|
55
|
+
const { sign, verify, getStore } = require("secure-web-token");
|
|
56
|
+
```
|
|
60
57
|
|
|
61
58
|
---
|
|
62
59
|
|
|
63
|
-
##
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### 1. Sign a Token (Login)
|
|
64
63
|
|
|
65
|
-
### `sign()` function
|
|
66
64
|
```ts
|
|
67
65
|
import { sign } from "secure-web-token";
|
|
68
66
|
|
|
69
|
-
const SECRET = "
|
|
67
|
+
const SECRET = "your-256-bit-secret";
|
|
70
68
|
|
|
71
69
|
const { token, sessionId } = sign(
|
|
72
70
|
{ userId: 1, role: "admin" },
|
|
73
71
|
SECRET,
|
|
74
72
|
{
|
|
75
|
-
fingerprint: true,
|
|
76
|
-
store: "memory",
|
|
77
|
-
expiresIn: 3600,
|
|
73
|
+
fingerprint: true, // bind to device
|
|
74
|
+
store: "memory", // server-side session store
|
|
75
|
+
expiresIn: 3600, // 1 hour
|
|
78
76
|
}
|
|
79
77
|
);
|
|
78
|
+
|
|
79
|
+
// Send `token` to client, store `sessionId` in HttpOnly cookie
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
### 2. Verify a Token (Protected Route)
|
|
83
83
|
|
|
84
|
-
### `verify()` function
|
|
85
84
|
```ts
|
|
86
85
|
import { verify, getStore } from "secure-web-token";
|
|
87
86
|
|
|
88
87
|
const store = getStore("memory");
|
|
89
|
-
const session = store.getSession(sessionId);
|
|
88
|
+
const session = store.getSession(sessionId); // from HttpOnly cookie
|
|
90
89
|
|
|
91
90
|
const payload = verify(token, SECRET, {
|
|
92
91
|
sessionId,
|
|
93
92
|
fingerprint: session.fingerprint,
|
|
94
93
|
store: "memory",
|
|
95
94
|
});
|
|
95
|
+
|
|
96
|
+
// payload.data => { userId: 1, role: "admin" }
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
---
|
|
99
100
|
|
|
100
|
-
##
|
|
101
|
+
## Full Express.js Example
|
|
101
102
|
|
|
102
|
-
### Backend (Express.js + Node.js)
|
|
103
103
|
```ts
|
|
104
104
|
import express from "express";
|
|
105
105
|
import cookieParser from "cookie-parser";
|
|
106
|
-
import cors from "cors";
|
|
107
106
|
import { sign, verify, getStore } from "secure-web-token";
|
|
108
107
|
|
|
109
108
|
const app = express();
|
|
110
|
-
app.use(cors({ origin: true, credentials: true }));
|
|
111
|
-
app.use(cookieParser());
|
|
112
109
|
app.use(express.json());
|
|
110
|
+
app.use(cookieParser());
|
|
113
111
|
|
|
114
|
-
const SECRET =
|
|
112
|
+
const SECRET = process.env.SWT_SECRET!;
|
|
115
113
|
const store = getStore("memory");
|
|
116
114
|
|
|
115
|
+
// Login โ issue SWT
|
|
117
116
|
app.post("/login", (req, res) => {
|
|
118
|
-
const user = { userId: 1, name: "
|
|
117
|
+
const user = { userId: 1, name: "Alice", role: "admin" };
|
|
119
118
|
|
|
120
119
|
const { token, sessionId } = sign(user, SECRET, {
|
|
121
120
|
fingerprint: true,
|
|
122
121
|
store: "memory",
|
|
122
|
+
expiresIn: 3600,
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// sessionId goes in an HttpOnly cookie โ never accessible to JS
|
|
126
|
+
res.cookie("swt_session", sessionId, { httpOnly: true, secure: true });
|
|
127
|
+
|
|
128
|
+
// Encrypted token goes to client
|
|
126
129
|
res.json({ token });
|
|
127
130
|
});
|
|
128
131
|
|
|
132
|
+
// Protected route โ verify SWT
|
|
129
133
|
app.get("/profile", (req, res) => {
|
|
130
134
|
try {
|
|
131
135
|
const sessionId = req.cookies.swt_session;
|
|
@@ -144,48 +148,23 @@ app.get("/profile", (req, res) => {
|
|
|
144
148
|
}
|
|
145
149
|
});
|
|
146
150
|
|
|
151
|
+
// Logout โ truly invalidates the session
|
|
152
|
+
app.post("/logout", (req, res) => {
|
|
153
|
+
const sessionId = req.cookies.swt_session;
|
|
154
|
+
store.deleteSession(sessionId);
|
|
155
|
+
res.clearCookie("swt_session");
|
|
156
|
+
res.json({ success: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
147
159
|
app.listen(4000);
|
|
148
160
|
```
|
|
149
161
|
|
|
150
|
-
|
|
151
|
-
```tsx
|
|
152
|
-
import { useState } from "react";
|
|
153
|
-
|
|
154
|
-
function App() {
|
|
155
|
-
const [user, setUser] = useState(null);
|
|
156
|
-
|
|
157
|
-
const login = async () => {
|
|
158
|
-
const res = await fetch("http://localhost:4000/login", {
|
|
159
|
-
method: "POST",
|
|
160
|
-
credentials: "include",
|
|
161
|
-
});
|
|
162
|
-
const data = await res.json();
|
|
163
|
-
localStorage.setItem("token", data.token);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const profile = async () => {
|
|
167
|
-
const token = localStorage.getItem("token");
|
|
168
|
-
const res = await fetch("http://localhost:4000/profile", {
|
|
169
|
-
credentials: "include",
|
|
170
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
171
|
-
});
|
|
172
|
-
console.log(await res.json());
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<>
|
|
177
|
-
<button onClick={login}>Login</button>
|
|
178
|
-
<button onClick={profile}>View Profile</button>
|
|
179
|
-
</>
|
|
180
|
-
);
|
|
181
|
-
}
|
|
162
|
+
---
|
|
182
163
|
|
|
183
|
-
|
|
184
|
-
```
|
|
164
|
+
## Token Payload Structure
|
|
185
165
|
|
|
186
|
-
|
|
166
|
+
The payload sent to the client is **fully AES-256-GCM encrypted**. Internally it looks like:
|
|
187
167
|
|
|
188
|
-
## 6. Payload Structure
|
|
189
168
|
```json
|
|
190
169
|
{
|
|
191
170
|
"data": {
|
|
@@ -194,31 +173,120 @@ export default App;
|
|
|
194
173
|
},
|
|
195
174
|
"iat": 1768368114,
|
|
196
175
|
"exp": 1768369014,
|
|
197
|
-
"fp": "device-id"
|
|
176
|
+
"fp": "device-fingerprint-id"
|
|
198
177
|
}
|
|
199
178
|
```
|
|
200
179
|
|
|
180
|
+
Unlike JWT, **this is not readable** by decoding the token in the browser or on another server.
|
|
181
|
+
|
|
201
182
|
---
|
|
202
183
|
|
|
203
|
-
##
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
184
|
+
## SWT vs JWT โ Deep Comparison
|
|
185
|
+
|
|
186
|
+
### JWT Security Flaws (Why You Should Replace JWT)
|
|
187
|
+
|
|
188
|
+
**1. Payloads are not encrypted.**
|
|
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.
|
|
190
|
+
|
|
191
|
+
**2. No device binding.**
|
|
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.
|
|
193
|
+
|
|
194
|
+
**3. Logout does not work (by design).**
|
|
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.
|
|
196
|
+
|
|
197
|
+
**4. Token theft = full session compromise.**
|
|
198
|
+
If a JWT is stolen via XSS or network interception, the attacker has full access for the token's entire lifetime.
|
|
199
|
+
|
|
200
|
+
### How SWT Fixes Every One of These
|
|
201
|
+
|
|
202
|
+
| JWT Flaw | SWT Solution |
|
|
203
|
+
|---|---|
|
|
204
|
+
| Readable payload | AES-256-GCM encryption โ payload is unreadable without the server secret |
|
|
205
|
+
| No device binding | Device fingerprint stored in server session โ token only valid on original device |
|
|
206
|
+
| Logout doesn't work | Server-side session deletion โ revocation is instant and permanent |
|
|
207
|
+
| Token theft | Stolen token cannot be used without matching device fingerprint + server session |
|
|
207
208
|
|
|
208
209
|
---
|
|
209
210
|
|
|
210
|
-
##
|
|
211
|
-
```ts
|
|
212
|
-
// ESM
|
|
213
|
-
import { sign, verify, getStore } from "secure-web-token";
|
|
211
|
+
## Frequently Asked Questions
|
|
214
212
|
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
**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
|
+
|
|
216
|
+
**Q: What encryption does SWT use?**
|
|
217
|
+
A: AES-256-GCM โ the gold standard for symmetric authenticated encryption. The same algorithm used by TLS 1.3.
|
|
218
|
+
|
|
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.
|
|
221
|
+
|
|
222
|
+
**Q: Does using server-side sessions make SWT stateful?**
|
|
223
|
+
A: Yes โ intentionally. The "stateless = good" assumption of JWT trades security for scalability. SWT recovers security while maintaining a minimal server-side footprint. For most applications, this is the correct tradeoff.
|
|
224
|
+
|
|
225
|
+
**Q: When should I still use JWT?**
|
|
226
|
+
A: JWT is acceptable for low-security, public-data, short-lived tokens between internal microservices where token interception risk is low. For anything user-facing, SWT is the better choice.
|
|
227
|
+
|
|
228
|
+
**Q: What Node.js version is required?**
|
|
229
|
+
A: Node.js 16+ (uses native `crypto` module for AES-256-GCM).
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Security Architecture
|
|
234
|
+
|
|
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
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
|
|
217
261
|
```
|
|
218
262
|
|
|
219
263
|
---
|
|
220
264
|
|
|
221
|
-
|
|
265
|
+
## Roadmap
|
|
266
|
+
|
|
267
|
+
- [x] AES-256-GCM payload encryption
|
|
268
|
+
- [x] Device fingerprint binding
|
|
269
|
+
- [x] Memory session store
|
|
270
|
+
- [x] Token expiry (`iat`, `exp`)
|
|
271
|
+
- [ ] Redis session store adapter
|
|
272
|
+
- [ ] Token rotation / refresh
|
|
273
|
+
- [ ] TypeScript types (strict)
|
|
274
|
+
- [ ] Express.js middleware helper
|
|
275
|
+
- [ ] Audit log support
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Contributing
|
|
280
|
+
|
|
281
|
+
PRs and issues welcome. If you find a security vulnerability, please open a private security advisory rather than a public issue.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT
|
|
288
|
+
|
|
289
|
+
---
|
|
222
290
|
|
|
223
|
-
|
|
224
|
-
|
|
291
|
+
> **Stop using JWT for sensitive user sessions. Your users deserve better.**
|
|
292
|
+
> `npm install secure-web-token`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "secure-web-token",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "A secure web token utility",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -31,4 +31,4 @@
|
|
|
31
31
|
"@types/node": "^25.0.6",
|
|
32
32
|
"typescript": "^5.9.3"
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
}
|