@fonderie/auth 7.14.4 → 7.15.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 +20 -0
- package/brain/signatures.md +1 -0
- package/dist/index.cjs +31 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +36 -13
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +15 -4
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js +16 -5
- package/dist/middlewares/index.js.map +1 -1
- package/dist/{session-BBF5t-3Y.d.cts → session-auNb2L8m.d.cts} +1 -0
- package/dist/{session-BBF5t-3Y.d.ts → session-auNb2L8m.d.ts} +1 -0
- package/env.json +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,26 @@ request; its output is sanitized and bounded (coordinates ~1 km); if it throws o
|
|
|
68
68
|
and the request is unaffected. Country is reliable; region and city are
|
|
69
69
|
approximate.
|
|
70
70
|
|
|
71
|
+
## Rotating the signing secret (nobody is signed out)
|
|
72
|
+
|
|
73
|
+
Every token carries the key id of the secret that signed it, and tokens are
|
|
74
|
+
verified against the current secret **and** any previous ones:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
new AuthModule(store, {
|
|
78
|
+
jwtSecret: process.env.JWT_SECRET!, // signs new tokens
|
|
79
|
+
jwtPreviousSecrets: process.env.JWT_PREVIOUS_SECRETS?.split(',') ?? [], // still verify
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
1. Put the current secret in `JWT_PREVIOUS_SECRETS` and a new one in `JWT_SECRET`; deploy.
|
|
84
|
+
2. Signed-in users keep working; their next refresh gets tokens signed with the new key.
|
|
85
|
+
3. After the longest session lifetime (`sessionDuration`), remove the old secret from
|
|
86
|
+
`JWT_PREVIOUS_SECRETS` and deploy again.
|
|
87
|
+
|
|
88
|
+
A weak previous secret fails readiness like a weak current one: it still verifies tokens.
|
|
89
|
+
See `docs/SESSION-DESIGN.md` for the session roadmap.
|
|
90
|
+
|
|
71
91
|
## Why this exists
|
|
72
92
|
|
|
73
93
|
You've shipped this plumbing before — auth, teams, billing, messaging —
|
package/brain/signatures.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -331,6 +331,12 @@ var EVENT_KEYS = {
|
|
|
331
331
|
// src/services/jwt.ts
|
|
332
332
|
var import_node_crypto = require("crypto");
|
|
333
333
|
var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
|
|
334
|
+
function keyIdOf(secret) {
|
|
335
|
+
return (0, import_node_crypto.createHash)("sha256").update(secret).digest("hex").slice(0, 16);
|
|
336
|
+
}
|
|
337
|
+
function keyRing(config) {
|
|
338
|
+
return [config.jwtSecret, ...config.jwtPreviousSecrets ?? []].filter((s) => typeof s === "string" && s.length > 0);
|
|
339
|
+
}
|
|
334
340
|
function issueMfaPendingToken(userId, config, loginMethod) {
|
|
335
341
|
return import_jsonwebtoken.default.sign(
|
|
336
342
|
{
|
|
@@ -341,7 +347,7 @@ function issueMfaPendingToken(userId, config, loginMethod) {
|
|
|
341
347
|
mfaPending: true
|
|
342
348
|
},
|
|
343
349
|
config.jwtSecret,
|
|
344
|
-
{ expiresIn: "5m" }
|
|
350
|
+
{ expiresIn: "5m", keyid: keyIdOf(config.jwtSecret) }
|
|
345
351
|
);
|
|
346
352
|
}
|
|
347
353
|
function issueTokenPair(userId, config, options) {
|
|
@@ -353,12 +359,12 @@ function issueTokenPair(userId, config, options) {
|
|
|
353
359
|
const accessToken = import_jsonwebtoken.default.sign(
|
|
354
360
|
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
355
361
|
config.jwtSecret,
|
|
356
|
-
{ expiresIn: accessDuration }
|
|
362
|
+
{ expiresIn: accessDuration, keyid: keyIdOf(config.jwtSecret) }
|
|
357
363
|
);
|
|
358
364
|
const refreshToken = import_jsonwebtoken.default.sign(
|
|
359
365
|
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
360
366
|
config.jwtSecret,
|
|
361
|
-
{ expiresIn: duration }
|
|
367
|
+
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret) }
|
|
362
368
|
);
|
|
363
369
|
return { accessToken, refreshToken, sid };
|
|
364
370
|
}
|
|
@@ -367,11 +373,16 @@ function refreshTokenExpiry(token) {
|
|
|
367
373
|
return decoded?.exp ? new Date(decoded.exp * 1e3) : new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
368
374
|
}
|
|
369
375
|
function verifyToken(token, config) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
376
|
+
const ring = keyRing(config);
|
|
377
|
+
const kid = import_jsonwebtoken.default.decode(token, { complete: true })?.header?.kid;
|
|
378
|
+
const candidates = typeof kid === "string" ? ring.filter((s) => keyIdOf(s) === kid) : ring;
|
|
379
|
+
for (const secret of candidates) {
|
|
380
|
+
try {
|
|
381
|
+
return import_jsonwebtoken.default.verify(token, secret);
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
374
384
|
}
|
|
385
|
+
return null;
|
|
375
386
|
}
|
|
376
387
|
|
|
377
388
|
// src/services/mfa.ts
|
|
@@ -3271,6 +3282,18 @@ function collectAuthConfigProblems(config) {
|
|
|
3271
3282
|
reason: "JWT_SECRET_PLACEHOLDER"
|
|
3272
3283
|
});
|
|
3273
3284
|
}
|
|
3285
|
+
(config.jwtPreviousSecrets ?? []).forEach((previous, index) => {
|
|
3286
|
+
if ((0, import_core11.secretStrengthProblem)(previous ?? "")) {
|
|
3287
|
+
problems.push({
|
|
3288
|
+
module: MODULE,
|
|
3289
|
+
severity: "error",
|
|
3290
|
+
message: `jwtPreviousSecrets[${index}] is too short or a placeholder \u2014 it still verifies tokens`,
|
|
3291
|
+
domain: "auth",
|
|
3292
|
+
reason: "JWT_PREVIOUS_SECRET_WEAK",
|
|
3293
|
+
metadata: { index }
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3274
3297
|
if (config.secureCookies === false) {
|
|
3275
3298
|
problems.push({
|
|
3276
3299
|
module: MODULE,
|
|
@@ -3556,7 +3579,7 @@ var AuthModule = class {
|
|
|
3556
3579
|
name = "@fonderie/auth";
|
|
3557
3580
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3558
3581
|
// say what is actually deployed rather than 'not reported'.
|
|
3559
|
-
version = "7.
|
|
3582
|
+
version = "7.15.0";
|
|
3560
3583
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3561
3584
|
describeAdmin() {
|
|
3562
3585
|
return { routes: describeAuthAdminRoutes(this.store) };
|