@schalkneethling/toolkit 0.5.1 → 0.6.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/dist/index.mjs.map +1 -1
- package/hooks/auto-approve-safe-commands/hook.mjs +5 -1
- package/hooks/auto-approve-safe-commands/hook.mts +7 -6
- package/hooks/block-dangerous-commands/hook.mjs +3 -3
- package/hooks/block-dangerous-commands/hook.mts +10 -22
- package/package.json +1 -1
- package/skills/css-tokens/SKILL.md +1 -1
- package/skills/css-tokens/references/tokens.css +6 -10
- package/skills/frontend-security/SKILL.md +3 -0
- package/skills/frontend-security/references/csp-configuration.md +68 -51
- package/skills/frontend-security/references/csrf-protection.md +74 -70
- package/skills/frontend-security/references/dom-security.md +36 -29
- package/skills/frontend-security/references/file-upload-security.md +101 -69
- package/skills/frontend-security/references/framework-patterns.md +42 -40
- package/skills/frontend-security/references/input-validation.md +36 -31
- package/skills/frontend-security/references/jwt-security.md +68 -84
- package/skills/frontend-security/references/nodejs-npm-security.md +63 -55
- package/skills/frontend-security/references/xss-prevention.md +38 -36
- package/skills/frontend-testing/SKILL.md +31 -38
- package/skills/frontend-testing/references/accessibility-testing.md +56 -62
- package/skills/frontend-testing/references/aria-snapshots.md +35 -34
- package/skills/frontend-testing/references/locator-strategies.md +37 -40
- package/skills/frontend-testing/references/visual-regression.md +29 -23
- package/skills/more-secure-dependabot-config/SKILL.md +120 -0
- package/skills/more-secure-dependabot-config/references/ecosystem.md +35 -0
- package/skills/npm-publishing-best-practices/SKILL.md +316 -0
- package/skills/semantic-html/SKILL.md +5 -21
- package/skills/semantic-html/references/heading-patterns.md +1 -5
|
@@ -15,8 +15,8 @@ function validateUsername(input) {
|
|
|
15
15
|
|
|
16
16
|
// AVOID: Denylist (block known bad)
|
|
17
17
|
function validateInput(input) {
|
|
18
|
-
const blocked = [
|
|
19
|
-
return !blocked.some(bad => input.includes(bad)); // Easily bypassed
|
|
18
|
+
const blocked = ["<script>", "javascript:", "onerror"];
|
|
19
|
+
return !blocked.some((bad) => input.includes(bad)); // Easily bypassed
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -33,8 +33,8 @@ function validateEmail(email) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Use built-in browser validation
|
|
36
|
-
const input = document.createElement(
|
|
37
|
-
input.type =
|
|
36
|
+
const input = document.createElement("input");
|
|
37
|
+
input.type = "email";
|
|
38
38
|
input.value = email;
|
|
39
39
|
return input.checkValidity();
|
|
40
40
|
```
|
|
@@ -45,7 +45,7 @@ return input.checkValidity();
|
|
|
45
45
|
function validateUrl(input) {
|
|
46
46
|
try {
|
|
47
47
|
const url = new URL(input);
|
|
48
|
-
return [
|
|
48
|
+
return ["http:", "https:"].includes(url.protocol);
|
|
49
49
|
} catch {
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
@@ -57,8 +57,8 @@ function validateSafeUrl(input) {
|
|
|
57
57
|
if (!url) return false;
|
|
58
58
|
|
|
59
59
|
// Block data: and javascript: schemes
|
|
60
|
-
const dangerous = [
|
|
61
|
-
return !dangerous.some(scheme => input.toLowerCase().startsWith(scheme));
|
|
60
|
+
const dangerous = ["javascript:", "data:", "vbscript:"];
|
|
61
|
+
return !dangerous.some((scheme) => input.toLowerCase().startsWith(scheme));
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
@@ -77,7 +77,7 @@ function validateDecimal(input, min, max, decimals) {
|
|
|
77
77
|
if (isNaN(num)) return false;
|
|
78
78
|
if (num < min || num > max) return false;
|
|
79
79
|
|
|
80
|
-
const parts = input.split(
|
|
80
|
+
const parts = input.split(".");
|
|
81
81
|
if (parts.length > 2) return false;
|
|
82
82
|
if (parts[1] && parts[1].length > decimals) return false;
|
|
83
83
|
|
|
@@ -107,7 +107,7 @@ function validateDateRange(input, minDate, maxDate) {
|
|
|
107
107
|
function validatePhone(input) {
|
|
108
108
|
// E.164 format: +[country][number], max 15 digits
|
|
109
109
|
const pattern = /^\+[1-9]\d{1,14}$/;
|
|
110
|
-
return pattern.test(input.replace(/[\s\-()]/g,
|
|
110
|
+
return pattern.test(input.replace(/[\s\-()]/g, ""));
|
|
111
111
|
}
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -118,14 +118,14 @@ function validatePhone(input) {
|
|
|
118
118
|
```javascript
|
|
119
119
|
function escapeHtml(input) {
|
|
120
120
|
const map = {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
'"':
|
|
125
|
-
"'":
|
|
126
|
-
|
|
121
|
+
"&": "&",
|
|
122
|
+
"<": "<",
|
|
123
|
+
">": ">",
|
|
124
|
+
'"': """,
|
|
125
|
+
"'": "'",
|
|
126
|
+
"/": "/",
|
|
127
127
|
};
|
|
128
|
-
return String(input).replace(/[&<>"'/]/g, char => map[char]);
|
|
128
|
+
return String(input).replace(/[&<>"'/]/g, (char) => map[char]);
|
|
129
129
|
}
|
|
130
130
|
```
|
|
131
131
|
|
|
@@ -136,14 +136,14 @@ function escapeHtml(input) {
|
|
|
136
136
|
const query = `SELECT * FROM users WHERE name = '${userInput}'`;
|
|
137
137
|
|
|
138
138
|
// RIGHT - use parameterized queries
|
|
139
|
-
const query =
|
|
139
|
+
const query = "SELECT * FROM users WHERE name = ?";
|
|
140
140
|
db.query(query, [userInput]);
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
### Path Traversal Prevention
|
|
144
144
|
|
|
145
145
|
```javascript
|
|
146
|
-
const path = require(
|
|
146
|
+
const path = require("path");
|
|
147
147
|
|
|
148
148
|
function validateFilePath(userPath, baseDir) {
|
|
149
149
|
const baseCanonical = path.resolve(baseDir);
|
|
@@ -151,8 +151,8 @@ function validateFilePath(userPath, baseDir) {
|
|
|
151
151
|
const relativePath = path.relative(baseCanonical, resolved);
|
|
152
152
|
|
|
153
153
|
// Ensure resolved path stays inside the base directory
|
|
154
|
-
if (relativePath.startsWith(
|
|
155
|
-
throw new Error(
|
|
154
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
155
|
+
throw new Error("Path traversal detected");
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
return resolved;
|
|
@@ -164,13 +164,13 @@ function validateFilePath(userPath, baseDir) {
|
|
|
164
164
|
### Node.js with Joi
|
|
165
165
|
|
|
166
166
|
```javascript
|
|
167
|
-
const Joi = require(
|
|
167
|
+
const Joi = require("joi");
|
|
168
168
|
|
|
169
169
|
const userSchema = Joi.object({
|
|
170
170
|
username: Joi.string().alphanum().min(3).max(30).required(),
|
|
171
171
|
email: Joi.string().email().required(),
|
|
172
172
|
age: Joi.number().integer().min(0).max(150),
|
|
173
|
-
website: Joi.string().uri({ scheme: [
|
|
173
|
+
website: Joi.string().uri({ scheme: ["http", "https"] }),
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
function validateUser(data) {
|
|
@@ -183,32 +183,37 @@ function validateUser(data) {
|
|
|
183
183
|
### Express Validator
|
|
184
184
|
|
|
185
185
|
```javascript
|
|
186
|
-
const { body, validationResult } = require(
|
|
186
|
+
const { body, validationResult } = require("express-validator");
|
|
187
187
|
|
|
188
|
-
app.post(
|
|
189
|
-
|
|
190
|
-
body(
|
|
191
|
-
body(
|
|
188
|
+
app.post(
|
|
189
|
+
"/user",
|
|
190
|
+
body("email").isEmail().normalizeEmail(),
|
|
191
|
+
body("password").isLength({ min: 8 }),
|
|
192
|
+
body("age").isInt({ min: 0, max: 150 }),
|
|
192
193
|
(req, res) => {
|
|
193
194
|
const errors = validationResult(req);
|
|
194
195
|
if (!errors.isEmpty()) {
|
|
195
196
|
return res.status(400).json({ errors: errors.array() });
|
|
196
197
|
}
|
|
197
198
|
// Process valid input
|
|
198
|
-
}
|
|
199
|
+
},
|
|
199
200
|
);
|
|
200
201
|
```
|
|
201
202
|
|
|
202
203
|
### Zod (TypeScript)
|
|
203
204
|
|
|
204
205
|
```typescript
|
|
205
|
-
import { z } from
|
|
206
|
+
import { z } from "zod";
|
|
206
207
|
|
|
207
208
|
const UserSchema = z.object({
|
|
208
|
-
username: z
|
|
209
|
+
username: z
|
|
210
|
+
.string()
|
|
211
|
+
.min(3)
|
|
212
|
+
.max(30)
|
|
213
|
+
.regex(/^[a-zA-Z0-9_]+$/),
|
|
209
214
|
email: z.string().email(),
|
|
210
215
|
age: z.number().int().min(0).max(150).optional(),
|
|
211
|
-
website: z.string().url().optional()
|
|
216
|
+
website: z.string().url().optional(),
|
|
212
217
|
});
|
|
213
218
|
|
|
214
219
|
type User = z.infer<typeof UserSchema>;
|
|
@@ -12,7 +12,7 @@ const decoded = jwt.verify(token, secret);
|
|
|
12
12
|
|
|
13
13
|
// SECURE - explicitly specify allowed algorithms
|
|
14
14
|
const decoded = jwt.verify(token, secret, {
|
|
15
|
-
algorithms: [
|
|
15
|
+
algorithms: ["HS256"], // Only accept HS256
|
|
16
16
|
});
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -26,7 +26,7 @@ const decoded = jwt.verify(token, publicKey);
|
|
|
26
26
|
|
|
27
27
|
// SECURE - specify expected algorithm
|
|
28
28
|
const decoded = jwt.verify(token, publicKey, {
|
|
29
|
-
algorithms: [
|
|
29
|
+
algorithms: ["RS256"], // Only accept RS256
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -36,16 +36,16 @@ Attack: Brute-force weak HMAC secrets.
|
|
|
36
36
|
|
|
37
37
|
```javascript
|
|
38
38
|
// VULNERABLE - weak secret
|
|
39
|
-
const token = jwt.sign(payload,
|
|
39
|
+
const token = jwt.sign(payload, "password123");
|
|
40
40
|
|
|
41
41
|
// SECURE - strong random secret (256+ bits)
|
|
42
|
-
const crypto = require(
|
|
43
|
-
const secret = crypto.randomBytes(32);
|
|
42
|
+
const crypto = require("crypto");
|
|
43
|
+
const secret = crypto.randomBytes(32); // 256 bits
|
|
44
44
|
const token = jwt.sign(payload, secret);
|
|
45
45
|
|
|
46
46
|
// Or use RSA keys
|
|
47
|
-
const privateKey = fs.readFileSync(
|
|
48
|
-
const token = jwt.sign(payload, privateKey, { algorithm:
|
|
47
|
+
const privateKey = fs.readFileSync("private.pem");
|
|
48
|
+
const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
## Token Sidejacking Prevention
|
|
@@ -53,46 +53,36 @@ const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
|
|
|
53
53
|
Add fingerprint to prevent stolen token reuse:
|
|
54
54
|
|
|
55
55
|
```javascript
|
|
56
|
-
const crypto = require(
|
|
56
|
+
const crypto = require("crypto");
|
|
57
57
|
|
|
58
58
|
// Generate fingerprint on login
|
|
59
59
|
function generateFingerprint() {
|
|
60
|
-
return crypto.randomBytes(32).toString(
|
|
60
|
+
return crypto.randomBytes(32).toString("hex");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// Create token with fingerprint hash
|
|
64
64
|
function createToken(userId, fingerprint) {
|
|
65
|
-
const fingerprintHash = crypto
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.digest('hex');
|
|
69
|
-
|
|
70
|
-
return jwt.sign(
|
|
71
|
-
{ sub: userId, fph: fingerprintHash },
|
|
72
|
-
secret,
|
|
73
|
-
{ expiresIn: '15m' }
|
|
74
|
-
);
|
|
65
|
+
const fingerprintHash = crypto.createHash("sha256").update(fingerprint).digest("hex");
|
|
66
|
+
|
|
67
|
+
return jwt.sign({ sub: userId, fph: fingerprintHash }, secret, { expiresIn: "15m" });
|
|
75
68
|
}
|
|
76
69
|
|
|
77
70
|
// Set fingerprint as httpOnly cookie
|
|
78
|
-
res.cookie(
|
|
71
|
+
res.cookie("__Secure-Fgp", fingerprint, {
|
|
79
72
|
httpOnly: true,
|
|
80
73
|
secure: true,
|
|
81
|
-
sameSite:
|
|
82
|
-
maxAge: 15 * 60 * 1000
|
|
74
|
+
sameSite: "Strict",
|
|
75
|
+
maxAge: 15 * 60 * 1000, // Match token expiry
|
|
83
76
|
});
|
|
84
77
|
|
|
85
78
|
// Validate both token and fingerprint
|
|
86
79
|
function validateToken(token, fingerprintCookie) {
|
|
87
|
-
const decoded = jwt.verify(token, secret, { algorithms: [
|
|
80
|
+
const decoded = jwt.verify(token, secret, { algorithms: ["HS256"] });
|
|
88
81
|
|
|
89
|
-
const fingerprintHash = crypto
|
|
90
|
-
.createHash('sha256')
|
|
91
|
-
.update(fingerprintCookie)
|
|
92
|
-
.digest('hex');
|
|
82
|
+
const fingerprintHash = crypto.createHash("sha256").update(fingerprintCookie).digest("hex");
|
|
93
83
|
|
|
94
84
|
if (decoded.fph !== fingerprintHash) {
|
|
95
|
-
throw new Error(
|
|
85
|
+
throw new Error("Invalid fingerprint");
|
|
96
86
|
}
|
|
97
87
|
|
|
98
88
|
return decoded;
|
|
@@ -105,16 +95,16 @@ function validateToken(token, fingerprintCookie) {
|
|
|
105
95
|
|
|
106
96
|
```javascript
|
|
107
97
|
// Store token in sessionStorage
|
|
108
|
-
sessionStorage.setItem(
|
|
98
|
+
sessionStorage.setItem("token", jwt);
|
|
109
99
|
|
|
110
100
|
// Clear on logout
|
|
111
|
-
sessionStorage.removeItem(
|
|
101
|
+
sessionStorage.removeItem("token");
|
|
112
102
|
|
|
113
103
|
// Send in Authorization header
|
|
114
|
-
fetch(
|
|
104
|
+
fetch("/api/data", {
|
|
115
105
|
headers: {
|
|
116
|
-
|
|
117
|
-
}
|
|
106
|
+
Authorization: `Bearer ${sessionStorage.getItem("token")}`,
|
|
107
|
+
},
|
|
118
108
|
});
|
|
119
109
|
```
|
|
120
110
|
|
|
@@ -122,11 +112,11 @@ fetch('/api/data', {
|
|
|
122
112
|
|
|
123
113
|
```javascript
|
|
124
114
|
// Server sets cookie
|
|
125
|
-
res.cookie(
|
|
126
|
-
httpOnly: true,
|
|
127
|
-
secure: true,
|
|
128
|
-
sameSite:
|
|
129
|
-
maxAge: 15 * 60 * 1000
|
|
115
|
+
res.cookie("token", jwt, {
|
|
116
|
+
httpOnly: true, // Not accessible via JavaScript
|
|
117
|
+
secure: true, // HTTPS only
|
|
118
|
+
sameSite: "Strict", // CSRF protection
|
|
119
|
+
maxAge: 15 * 60 * 1000,
|
|
130
120
|
});
|
|
131
121
|
|
|
132
122
|
// Need CSRF protection with cookie-based auth
|
|
@@ -136,50 +126,42 @@ res.cookie('token', jwt, {
|
|
|
136
126
|
|
|
137
127
|
```javascript
|
|
138
128
|
// VULNERABLE - persists after browser close, accessible to XSS
|
|
139
|
-
localStorage.setItem(
|
|
129
|
+
localStorage.setItem("token", jwt); // Not recommended
|
|
140
130
|
```
|
|
141
131
|
|
|
142
132
|
## Token Expiration
|
|
143
133
|
|
|
144
134
|
```javascript
|
|
145
135
|
// Short-lived access tokens (15-60 minutes)
|
|
146
|
-
const accessToken = jwt.sign(payload, secret, { expiresIn:
|
|
136
|
+
const accessToken = jwt.sign(payload, secret, { expiresIn: "15m" });
|
|
147
137
|
|
|
148
138
|
// Longer refresh tokens (days/weeks)
|
|
149
|
-
const refreshToken = jwt.sign(
|
|
150
|
-
{ sub: userId, type: 'refresh' },
|
|
151
|
-
refreshSecret,
|
|
152
|
-
{ expiresIn: '7d' }
|
|
153
|
-
);
|
|
139
|
+
const refreshToken = jwt.sign({ sub: userId, type: "refresh" }, refreshSecret, { expiresIn: "7d" });
|
|
154
140
|
|
|
155
141
|
// Refresh endpoint
|
|
156
|
-
app.post(
|
|
142
|
+
app.post("/refresh", async (req, res) => {
|
|
157
143
|
const { refreshToken } = req.body;
|
|
158
144
|
|
|
159
145
|
try {
|
|
160
146
|
const decoded = jwt.verify(refreshToken, refreshSecret, {
|
|
161
|
-
algorithms: [
|
|
147
|
+
algorithms: ["HS256"],
|
|
162
148
|
});
|
|
163
149
|
|
|
164
|
-
if (decoded.type !==
|
|
165
|
-
throw new Error(
|
|
150
|
+
if (decoded.type !== "refresh") {
|
|
151
|
+
throw new Error("Invalid token type");
|
|
166
152
|
}
|
|
167
153
|
|
|
168
154
|
// Check if refresh token is revoked
|
|
169
155
|
if (await isTokenRevoked(decoded)) {
|
|
170
|
-
throw new Error(
|
|
156
|
+
throw new Error("Token revoked");
|
|
171
157
|
}
|
|
172
158
|
|
|
173
159
|
// Issue new access token
|
|
174
|
-
const newAccessToken = jwt.sign(
|
|
175
|
-
{ sub: decoded.sub },
|
|
176
|
-
secret,
|
|
177
|
-
{ expiresIn: '15m' }
|
|
178
|
-
);
|
|
160
|
+
const newAccessToken = jwt.sign({ sub: decoded.sub }, secret, { expiresIn: "15m" });
|
|
179
161
|
|
|
180
162
|
res.json({ accessToken: newAccessToken });
|
|
181
163
|
} catch (error) {
|
|
182
|
-
res.status(401).json({ error:
|
|
164
|
+
res.status(401).json({ error: "Invalid refresh token" });
|
|
183
165
|
}
|
|
184
166
|
});
|
|
185
167
|
```
|
|
@@ -190,15 +172,15 @@ JWTs are stateless, so revocation requires additional mechanisms:
|
|
|
190
172
|
|
|
191
173
|
```javascript
|
|
192
174
|
// Denylist approach
|
|
193
|
-
const revokedTokens = new Map();
|
|
175
|
+
const revokedTokens = new Map(); // Use Redis in production
|
|
194
176
|
|
|
195
177
|
function revokeToken(token) {
|
|
196
|
-
const decoded = jwt.verify(token, secret, { algorithms: [
|
|
178
|
+
const decoded = jwt.verify(token, secret, { algorithms: ["HS256"] });
|
|
197
179
|
const tokenId = decoded.jti;
|
|
198
180
|
const expiry = decoded.exp * 1000;
|
|
199
181
|
|
|
200
182
|
if (!tokenId || !expiry) {
|
|
201
|
-
throw new Error(
|
|
183
|
+
throw new Error("Token missing required revocation claims");
|
|
202
184
|
}
|
|
203
185
|
|
|
204
186
|
// Store until token expires
|
|
@@ -213,11 +195,7 @@ function isTokenRevoked(decoded) {
|
|
|
213
195
|
}
|
|
214
196
|
|
|
215
197
|
// Include jti (JWT ID) in tokens
|
|
216
|
-
const token = jwt.sign(
|
|
217
|
-
{ sub: userId, jti: crypto.randomUUID() },
|
|
218
|
-
secret,
|
|
219
|
-
{ expiresIn: '15m' }
|
|
220
|
-
);
|
|
198
|
+
const token = jwt.sign({ sub: userId, jti: crypto.randomUUID() }, secret, { expiresIn: "15m" });
|
|
221
199
|
```
|
|
222
200
|
|
|
223
201
|
## Token Information Disclosure
|
|
@@ -226,17 +204,23 @@ JWTs are base64-encoded, not encrypted. Sensitive data is visible.
|
|
|
226
204
|
|
|
227
205
|
```javascript
|
|
228
206
|
// VULNERABLE - sensitive data in payload
|
|
229
|
-
const token = jwt.sign(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
207
|
+
const token = jwt.sign(
|
|
208
|
+
{
|
|
209
|
+
sub: userId,
|
|
210
|
+
ssn: "123-45-6789", // Visible to anyone!
|
|
211
|
+
salary: 100000,
|
|
212
|
+
},
|
|
213
|
+
secret,
|
|
214
|
+
);
|
|
234
215
|
|
|
235
216
|
// SECURE - minimal claims
|
|
236
|
-
const token = jwt.sign(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
217
|
+
const token = jwt.sign(
|
|
218
|
+
{
|
|
219
|
+
sub: userId,
|
|
220
|
+
role: "user",
|
|
221
|
+
},
|
|
222
|
+
secret,
|
|
223
|
+
);
|
|
240
224
|
|
|
241
225
|
// If sensitive data required, encrypt the token
|
|
242
226
|
const encryptedToken = encrypt(token, encryptionKey);
|
|
@@ -248,38 +232,38 @@ const encryptedToken = encrypt(token, encryptionKey);
|
|
|
248
232
|
function authenticateToken(req, res, next) {
|
|
249
233
|
// Get token from header
|
|
250
234
|
const authHeader = req.headers.authorization;
|
|
251
|
-
const token = authHeader?.
|
|
235
|
+
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
|
|
252
236
|
|
|
253
237
|
if (!token) {
|
|
254
|
-
return res.status(401).json({ error:
|
|
238
|
+
return res.status(401).json({ error: "Token required" });
|
|
255
239
|
}
|
|
256
240
|
|
|
257
241
|
try {
|
|
258
242
|
// Verify with explicit algorithm
|
|
259
243
|
const decoded = jwt.verify(token, secret, {
|
|
260
|
-
algorithms: [
|
|
261
|
-
issuer:
|
|
262
|
-
audience:
|
|
244
|
+
algorithms: ["HS256"],
|
|
245
|
+
issuer: "myapp",
|
|
246
|
+
audience: "myapp-users",
|
|
263
247
|
});
|
|
264
248
|
|
|
265
249
|
// Validate fingerprint if using sidejacking protection
|
|
266
|
-
const fingerprint = req.cookies[
|
|
250
|
+
const fingerprint = req.cookies["__Secure-Fgp"];
|
|
267
251
|
if (!validateFingerprint(decoded, fingerprint)) {
|
|
268
|
-
throw new Error(
|
|
252
|
+
throw new Error("Invalid fingerprint");
|
|
269
253
|
}
|
|
270
254
|
|
|
271
255
|
// Check revocation
|
|
272
256
|
if (isTokenRevoked(decoded)) {
|
|
273
|
-
throw new Error(
|
|
257
|
+
throw new Error("Token revoked");
|
|
274
258
|
}
|
|
275
259
|
|
|
276
260
|
req.user = decoded;
|
|
277
261
|
next();
|
|
278
262
|
} catch (error) {
|
|
279
|
-
if (error.name ===
|
|
280
|
-
return res.status(401).json({ error:
|
|
263
|
+
if (error.name === "TokenExpiredError") {
|
|
264
|
+
return res.status(401).json({ error: "Token expired" });
|
|
281
265
|
}
|
|
282
|
-
return res.status(403).json({ error:
|
|
266
|
+
return res.status(403).json({ error: "Invalid token" });
|
|
283
267
|
}
|
|
284
268
|
}
|
|
285
269
|
```
|