@fonderie/auth 7.15.0 → 7.16.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/brain/outcomes.md +3 -0
- package/dist/index.cjs +121 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +107 -31
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +65 -6
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +65 -6
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/sql/020_session_rotation.sql +22 -0
- package/package.json +1 -1
package/brain/outcomes.md
CHANGED
|
@@ -95,6 +95,9 @@ expires_at TIMESTAMPTZ NOT NULL
|
|
|
95
95
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
96
96
|
sid UUID
|
|
97
97
|
location JSONB
|
|
98
|
+
previous_token_hash TEXT
|
|
99
|
+
previous_valid_until TIMESTAMPTZ
|
|
100
|
+
last_used_at TIMESTAMPTZ
|
|
98
101
|
-- INDEX idx_fonderie_sessions_expires_at (expires_at)
|
|
99
102
|
-- INDEX idx_fonderie_sessions_sid (sid)
|
|
100
103
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -325,7 +325,10 @@ var EVENT_KEYS = {
|
|
|
325
325
|
// Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.
|
|
326
326
|
userPurged: "fonderie.user.purged",
|
|
327
327
|
emailVerified: "fonderie.user.email_verified",
|
|
328
|
-
passwordChanged: "fonderie.user.password_changed"
|
|
328
|
+
passwordChanged: "fonderie.user.password_changed",
|
|
329
|
+
// A session the server revoked — today: a refresh token reused after its
|
|
330
|
+
// grace (theft signal). { userId, sessionId, reason }.
|
|
331
|
+
sessionRevoked: "fonderie.session.revoked"
|
|
329
332
|
};
|
|
330
333
|
|
|
331
334
|
// src/services/jwt.ts
|
|
@@ -355,16 +358,19 @@ function issueTokenPair(userId, config, options) {
|
|
|
355
358
|
const accessDuration = config.accessTokenDuration ?? "24h";
|
|
356
359
|
const loginMethod = options.loginMethod;
|
|
357
360
|
const phoneVerified = options.phoneVerified ?? false;
|
|
358
|
-
const sid = (0, import_node_crypto.randomUUID)();
|
|
361
|
+
const sid = options.sid ?? (0, import_node_crypto.randomUUID)();
|
|
359
362
|
const accessToken = import_jsonwebtoken.default.sign(
|
|
360
363
|
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
361
364
|
config.jwtSecret,
|
|
362
|
-
|
|
365
|
+
// jwtid: every token is unique. With the session id kept across rotations,
|
|
366
|
+
// two tokens issued in the same second would otherwise be byte-identical —
|
|
367
|
+
// and a rotated-away token indistinguishable from the live one.
|
|
368
|
+
{ expiresIn: accessDuration, keyid: keyIdOf(config.jwtSecret), jwtid: (0, import_node_crypto.randomUUID)() }
|
|
363
369
|
);
|
|
364
370
|
const refreshToken = import_jsonwebtoken.default.sign(
|
|
365
371
|
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
366
372
|
config.jwtSecret,
|
|
367
|
-
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret) }
|
|
373
|
+
{ expiresIn: duration, keyid: keyIdOf(config.jwtSecret), jwtid: (0, import_node_crypto.randomUUID)() }
|
|
368
374
|
);
|
|
369
375
|
return { accessToken, refreshToken, sid };
|
|
370
376
|
}
|
|
@@ -908,6 +914,9 @@ var UserModel = class {
|
|
|
908
914
|
};
|
|
909
915
|
var MAX_LIST_LIMIT = 200;
|
|
910
916
|
|
|
917
|
+
// src/models/session.model.ts
|
|
918
|
+
var import_node_crypto4 = require("crypto");
|
|
919
|
+
|
|
911
920
|
// src/services/request-location.ts
|
|
912
921
|
var LOCATION_TIMEOUT_MS = 500;
|
|
913
922
|
var TEXT_MAX = 128;
|
|
@@ -990,6 +999,10 @@ async function resolveOnce(resolver, req, timeoutMs) {
|
|
|
990
999
|
}
|
|
991
1000
|
|
|
992
1001
|
// src/models/session.model.ts
|
|
1002
|
+
function hashRefreshToken(token) {
|
|
1003
|
+
return (0, import_node_crypto4.createHash)("sha256").update(token).digest("hex");
|
|
1004
|
+
}
|
|
1005
|
+
var REFRESH_GRACE_MS = 3e4;
|
|
993
1006
|
var SessionModel = class {
|
|
994
1007
|
// `locate` is IAuthConfig.location. Absent ⇒ sessions carry no location.
|
|
995
1008
|
constructor(store, locate) {
|
|
@@ -1001,12 +1014,12 @@ var SessionModel = class {
|
|
|
1001
1014
|
async create(userId, token, expiresAt, sid, meta) {
|
|
1002
1015
|
const location = this.locate && meta?.headers ? await resolveLocation(this.locate, { ip: meta.ipAddress, headers: meta.headers }) : null;
|
|
1003
1016
|
await this.store.query(
|
|
1004
|
-
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location)
|
|
1005
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
1017
|
+
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location, last_used_at)
|
|
1018
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
1006
1019
|
ON CONFLICT (token) DO NOTHING`,
|
|
1007
1020
|
[
|
|
1008
1021
|
userId,
|
|
1009
|
-
token,
|
|
1022
|
+
hashRefreshToken(token),
|
|
1010
1023
|
expiresAt,
|
|
1011
1024
|
sid ?? null,
|
|
1012
1025
|
meta?.userAgent ?? null,
|
|
@@ -1016,7 +1029,56 @@ var SessionModel = class {
|
|
|
1016
1029
|
);
|
|
1017
1030
|
}
|
|
1018
1031
|
async delete(token) {
|
|
1019
|
-
await this.store.query(`DELETE FROM fonderie_sessions WHERE token
|
|
1032
|
+
await this.store.query(`DELETE FROM fonderie_sessions WHERE token IN ($1, $2)`, [hashRefreshToken(token), token]);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Where a presented refresh token stands: the session's current token, its
|
|
1036
|
+
* previous one within the grace (a retry or race), its previous one AFTER
|
|
1037
|
+
* the grace (reuse — a theft signal), or nothing known.
|
|
1038
|
+
*/
|
|
1039
|
+
async match(token) {
|
|
1040
|
+
const hash = hashRefreshToken(token);
|
|
1041
|
+
const [current] = await this.store.query(
|
|
1042
|
+
`SELECT id, user_id, sid, token FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now() LIMIT 1`,
|
|
1043
|
+
[hash, token]
|
|
1044
|
+
);
|
|
1045
|
+
if (current) return { kind: "current", row: toRow(current) };
|
|
1046
|
+
const [previous] = await this.store.query(
|
|
1047
|
+
`SELECT id, user_id, sid, token, (previous_valid_until > now()) AS in_grace
|
|
1048
|
+
FROM fonderie_sessions WHERE previous_token_hash = $1 AND expires_at > now() LIMIT 1`,
|
|
1049
|
+
[hash]
|
|
1050
|
+
);
|
|
1051
|
+
if (!previous) return { kind: "unknown" };
|
|
1052
|
+
return { kind: previous.in_grace ? "grace" : "reused", row: toRow(previous) };
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Rotate a session in place to a new refresh token: same row, same device.
|
|
1056
|
+
* From the CURRENT token it is optimistic — only if the stored token is
|
|
1057
|
+
* still the one matched; a concurrent rotation makes it return false and the
|
|
1058
|
+
* caller re-matches (and now finds the token as the previous one). The old
|
|
1059
|
+
* token becomes the previous, valid for the grace. From the PREVIOUS token
|
|
1060
|
+
* (within the grace) it is authorised by that hash and deadline alone, so
|
|
1061
|
+
* racing retries never starve; the previous and its deadline are kept.
|
|
1062
|
+
*/
|
|
1063
|
+
async rotate(row, presented, next, from) {
|
|
1064
|
+
const rows = await this.store.query(
|
|
1065
|
+
`UPDATE fonderie_sessions
|
|
1066
|
+
SET previous_token_hash = CASE WHEN $5 THEN $6 ELSE previous_token_hash END,
|
|
1067
|
+
previous_valid_until = CASE WHEN $5 THEN now() + ($7 || ' milliseconds')::interval ELSE previous_valid_until END,
|
|
1068
|
+
token = $3,
|
|
1069
|
+
expires_at = $4,
|
|
1070
|
+
last_used_at = now()
|
|
1071
|
+
WHERE id = $1
|
|
1072
|
+
AND CASE WHEN $5 THEN token = $2
|
|
1073
|
+
ELSE previous_token_hash = $6 AND previous_valid_until > now() END
|
|
1074
|
+
RETURNING id`,
|
|
1075
|
+
[row.id, row.stored, hashRefreshToken(next.token), next.expiresAt, from === "current", hashRefreshToken(presented), String(REFRESH_GRACE_MS)]
|
|
1076
|
+
);
|
|
1077
|
+
return rows.length === 1;
|
|
1078
|
+
}
|
|
1079
|
+
/** Revoke one session by its row id (any user) — reuse detection, admin. */
|
|
1080
|
+
async revokeById(id) {
|
|
1081
|
+
await this.store.query(`DELETE FROM fonderie_sessions WHERE id = $1`, [id]);
|
|
1020
1082
|
}
|
|
1021
1083
|
// Delete the session an access token is bound to (by its sid claim). Lets
|
|
1022
1084
|
// logout revoke the current session without the client resending the refresh
|
|
@@ -1082,8 +1144,8 @@ var SessionModel = class {
|
|
|
1082
1144
|
}
|
|
1083
1145
|
async exists(token) {
|
|
1084
1146
|
const rows = await this.store.query(
|
|
1085
|
-
`SELECT id FROM fonderie_sessions WHERE token
|
|
1086
|
-
[token]
|
|
1147
|
+
`SELECT id FROM fonderie_sessions WHERE token IN ($1, $2) AND expires_at > now()`,
|
|
1148
|
+
[hashRefreshToken(token), token]
|
|
1087
1149
|
);
|
|
1088
1150
|
return rows.length > 0;
|
|
1089
1151
|
}
|
|
@@ -1096,6 +1158,9 @@ var SessionModel = class {
|
|
|
1096
1158
|
return rows.length > 0;
|
|
1097
1159
|
}
|
|
1098
1160
|
};
|
|
1161
|
+
function toRow(r) {
|
|
1162
|
+
return { id: r.id, userId: r.user_id, sid: r.sid, stored: r.token };
|
|
1163
|
+
}
|
|
1099
1164
|
|
|
1100
1165
|
// src/models/backup-code.model.ts
|
|
1101
1166
|
var BackupCodeModel = class {
|
|
@@ -1430,7 +1495,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1430
1495
|
}
|
|
1431
1496
|
|
|
1432
1497
|
// src/controllers/auth.controller.ts
|
|
1433
|
-
var
|
|
1498
|
+
var import_node_crypto6 = require("crypto");
|
|
1434
1499
|
var import_events2 = require("@fonderie/events");
|
|
1435
1500
|
var import_core6 = require("@fonderie/core");
|
|
1436
1501
|
|
|
@@ -1477,8 +1542,8 @@ function normalizeEmailSafe(email2) {
|
|
|
1477
1542
|
}
|
|
1478
1543
|
|
|
1479
1544
|
// src/models/password-reset.model.ts
|
|
1480
|
-
var
|
|
1481
|
-
var hashSecret = (value) => (0,
|
|
1545
|
+
var import_node_crypto5 = require("crypto");
|
|
1546
|
+
var hashSecret = (value) => (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
|
|
1482
1547
|
var PasswordResetModel = class {
|
|
1483
1548
|
constructor(store) {
|
|
1484
1549
|
this.store = store;
|
|
@@ -1672,7 +1737,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1672
1737
|
if (!row) {
|
|
1673
1738
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1674
1739
|
}
|
|
1675
|
-
const pin = (0,
|
|
1740
|
+
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
1676
1741
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1677
1742
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1678
1743
|
const user = await users.findById(row.id);
|
|
@@ -1743,7 +1808,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1743
1808
|
lastName ?? null,
|
|
1744
1809
|
locale
|
|
1745
1810
|
);
|
|
1746
|
-
const otp = (0,
|
|
1811
|
+
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
1747
1812
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1748
1813
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1749
1814
|
const user = await users.findById(id);
|
|
@@ -1887,7 +1952,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1887
1952
|
"Account suspended. Please contact support."
|
|
1888
1953
|
);
|
|
1889
1954
|
}
|
|
1890
|
-
const otp = (0,
|
|
1955
|
+
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
1891
1956
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1892
1957
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1893
1958
|
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1938,24 +2003,35 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1938
2003
|
if (!payload || payload.type !== "refresh") {
|
|
1939
2004
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
|
|
1940
2005
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
"TOKEN_REFRESH_FAILED",
|
|
1946
|
-
|
|
1947
|
-
)
|
|
2006
|
+
let issued = null;
|
|
2007
|
+
for (let attempt = 0; attempt < 3 && !issued; attempt++) {
|
|
2008
|
+
const found = await sessions.match(token);
|
|
2009
|
+
if (found.kind === "unknown") {
|
|
2010
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2011
|
+
}
|
|
2012
|
+
if (found.kind === "reused") {
|
|
2013
|
+
await sessions.revokeById(found.row.id);
|
|
2014
|
+
await (0, import_core6.background)(
|
|
2015
|
+
bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sessionId: found.row.id, reason: "refresh-reuse" })
|
|
2016
|
+
);
|
|
2017
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
2018
|
+
}
|
|
2019
|
+
const user = await users.findById(found.row.userId);
|
|
2020
|
+
if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
|
|
2021
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2022
|
+
}
|
|
2023
|
+
const pair = issueTokenPair(user.id, config, {
|
|
2024
|
+
loginMethod: payload.loginMethod ?? "email",
|
|
2025
|
+
phoneVerified: payload.phoneVerified ?? false,
|
|
2026
|
+
...found.row.sid ? { sid: found.row.sid } : {}
|
|
2027
|
+
});
|
|
2028
|
+
const next = { token: pair.refreshToken, expiresAt: refreshTokenExpiry(pair.refreshToken) };
|
|
2029
|
+
if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
|
|
1948
2030
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
2031
|
+
if (!issued) {
|
|
2032
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
|
|
1952
2033
|
}
|
|
1953
|
-
|
|
1954
|
-
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1955
|
-
loginMethod: payload.loginMethod ?? "email",
|
|
1956
|
-
phoneVerified: payload.phoneVerified ?? false
|
|
1957
|
-
});
|
|
1958
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
|
|
2034
|
+
const { accessToken, refreshToken } = issued;
|
|
1959
2035
|
return Response.json(
|
|
1960
2036
|
{
|
|
1961
2037
|
reason: "TOKENS_REFRESHED",
|
|
@@ -1996,8 +2072,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
1996
2072
|
"Password reset email sent (if account exists)."
|
|
1997
2073
|
);
|
|
1998
2074
|
}
|
|
1999
|
-
const pin = (0,
|
|
2000
|
-
const token = (0,
|
|
2075
|
+
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2076
|
+
const token = (0, import_node_crypto6.randomBytes)(32).toString("hex");
|
|
2001
2077
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60);
|
|
2002
2078
|
await passwordReset.create(user.id, pin, token, expiresAt);
|
|
2003
2079
|
const base = resolved.passwordResetUrl;
|
|
@@ -2203,7 +2279,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2203
2279
|
}
|
|
2204
2280
|
);
|
|
2205
2281
|
}
|
|
2206
|
-
const otp = (0,
|
|
2282
|
+
const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2207
2283
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
2208
2284
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
2209
2285
|
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -2242,7 +2318,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2242
2318
|
}
|
|
2243
2319
|
);
|
|
2244
2320
|
}
|
|
2245
|
-
const pin = (0,
|
|
2321
|
+
const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
|
|
2246
2322
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2247
2323
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2248
2324
|
await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -2259,7 +2335,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
|
|
|
2259
2335
|
}
|
|
2260
2336
|
|
|
2261
2337
|
// src/controllers/user.controller.ts
|
|
2262
|
-
var
|
|
2338
|
+
var import_node_crypto7 = require("crypto");
|
|
2263
2339
|
var import_core8 = require("@fonderie/core");
|
|
2264
2340
|
var import_events3 = require("@fonderie/events");
|
|
2265
2341
|
|
|
@@ -2453,7 +2529,7 @@ function userController(store, config, bus) {
|
|
|
2453
2529
|
if (existing) {
|
|
2454
2530
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
2455
2531
|
}
|
|
2456
|
-
const pin = (0,
|
|
2532
|
+
const pin = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
|
|
2457
2533
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
2458
2534
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
2459
2535
|
await users.updateEmail(ctx.user.id, normalised);
|
|
@@ -2495,7 +2571,7 @@ function userController(store, config, bus) {
|
|
|
2495
2571
|
if (existing) {
|
|
2496
2572
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
2497
2573
|
}
|
|
2498
|
-
const otp = (0,
|
|
2574
|
+
const otp = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
|
|
2499
2575
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
2500
2576
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
2501
2577
|
await users.updatePhone(ctx.user.id, normalised);
|
|
@@ -2645,7 +2721,7 @@ function userController(store, config, bus) {
|
|
|
2645
2721
|
}
|
|
2646
2722
|
|
|
2647
2723
|
// src/controllers/oauth.controller.ts
|
|
2648
|
-
var
|
|
2724
|
+
var import_node_crypto8 = require("crypto");
|
|
2649
2725
|
var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
|
|
2650
2726
|
var import_core9 = require("@fonderie/core");
|
|
2651
2727
|
var import_events4 = require("@fonderie/events");
|
|
@@ -2720,7 +2796,7 @@ async function getApplePublicKey(kid) {
|
|
|
2720
2796
|
}
|
|
2721
2797
|
if (!jwk) return null;
|
|
2722
2798
|
try {
|
|
2723
|
-
return (0,
|
|
2799
|
+
return (0, import_node_crypto8.createPublicKey)({ key: jwk, format: "jwk" });
|
|
2724
2800
|
} catch {
|
|
2725
2801
|
return null;
|
|
2726
2802
|
}
|
|
@@ -2878,7 +2954,7 @@ function oauthController(store, config, bus) {
|
|
|
2878
2954
|
"Google OAuth not configured"
|
|
2879
2955
|
);
|
|
2880
2956
|
}
|
|
2881
|
-
const state = (0,
|
|
2957
|
+
const state = (0, import_node_crypto8.randomBytes)(16).toString("hex");
|
|
2882
2958
|
const params = new URLSearchParams({
|
|
2883
2959
|
client_id: google.clientId,
|
|
2884
2960
|
redirect_uri: google.redirectUri,
|
|
@@ -3026,7 +3102,7 @@ function oauthController(store, config, bus) {
|
|
|
3026
3102
|
if (!apple) {
|
|
3027
3103
|
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
|
|
3028
3104
|
}
|
|
3029
|
-
const state = (0,
|
|
3105
|
+
const state = (0, import_node_crypto8.randomBytes)(16).toString("hex");
|
|
3030
3106
|
const params = new URLSearchParams({
|
|
3031
3107
|
client_id: apple.clientId,
|
|
3032
3108
|
redirect_uri: apple.redirectUri,
|
|
@@ -3122,7 +3198,7 @@ function oauthController(store, config, bus) {
|
|
|
3122
3198
|
});
|
|
3123
3199
|
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3124
3200
|
}
|
|
3125
|
-
const tokenHash = (0,
|
|
3201
|
+
const tokenHash = (0, import_node_crypto8.createHash)("sha256").update(identityToken).digest("hex");
|
|
3126
3202
|
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
3127
3203
|
const firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));
|
|
3128
3204
|
if (!firstUse) {
|
|
@@ -3579,7 +3655,7 @@ var AuthModule = class {
|
|
|
3579
3655
|
name = "@fonderie/auth";
|
|
3580
3656
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3581
3657
|
// say what is actually deployed rather than 'not reported'.
|
|
3582
|
-
version = "7.
|
|
3658
|
+
version = "7.16.0";
|
|
3583
3659
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3584
3660
|
describeAdmin() {
|
|
3585
3661
|
return { routes: describeAuthAdminRoutes(this.store) };
|