@tumbaland/backend-core 1.41.0 → 1.42.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.
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAS1E,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,MAAM,EACZ,MAAM,SAAS,CAAC;AAEjB;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,iFAAiF;IACjF,OAAO,EAAE,YAAY,CAAC;IACtB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,aAAa,CAAC;SACxB;KACF;CACF;AAgJD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAoElE,CAAC;AAEJ;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAiB7D,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,cASzB,CAAC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAS1E,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,MAAM,EACZ,MAAM,SAAS,CAAC;AAGjB;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,iFAAiF;IACjF,OAAO,EAAE,YAAY,CAAC;IACtB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,aAAa,CAAC;SACxB;KACF;CACF;AAgJD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAwElE,CAAC;AAEJ;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAiB7D,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,cASzB,CAAC"}
@@ -12,6 +12,7 @@ const tokens_1 = require("../oauth/tokens");
12
12
  const crypto_1 = require("./crypto");
13
13
  const service_1 = require("./service");
14
14
  const types_1 = require("./types");
15
+ const session_1 = require("../auth/session");
15
16
  /** Both auth paths read the token from the same two places. */
16
17
  const extractToken = (req) => req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
17
18
  const unauthorized = (res) => {
@@ -153,6 +154,10 @@ const authenticateAgent = (...requiredScopes) => async function authenticateAgen
153
154
  return;
154
155
  }
155
156
  if (!(0, tokens_1.isAccessTokenClaims)(claims)) {
157
+ if (!(await (0, session_1.isCurrentSession)(claims))) {
158
+ unauthorized(res);
159
+ return;
160
+ }
156
161
  const user = claims;
157
162
  req.user = user;
158
163
  req.userGroups = user.groups ?? [];
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * One document per signed-in device, owned and written by auth-service; every
3
+ * other service only reads it here. A session token names its document in the
4
+ * `sid` claim, and the token is only as good as that document: deleting it
5
+ * (sign-out, "sign out this device") ends the session on its next request, in
6
+ * every service, and its `expiresAt` (slid forward by auth-service while the
7
+ * device is in use, never past a hard cap from sign-in) is what idle expiry
8
+ * actually means.
9
+ */
10
+ export declare const USER_SESSIONS_COLLECTION = "user_sessions";
11
+ /** True for a pre-sessions token: no `sid`, and the old 24-hour lifetime at most. */
12
+ export declare function isLegacySessionlessToken(claims: unknown): boolean;
13
+ type SessionCheck = (claims: unknown) => Promise<boolean>;
14
+ export declare const isCurrentSession: (claims: unknown) => Promise<boolean>;
15
+ /**
16
+ * Replaces the check, for tests only; pass nothing to restore the real one.
17
+ * Not exported from the package index: service suites reach it through the
18
+ * `@tumbaland/backend-core/testing` entry point (see testing.js).
19
+ */
20
+ export declare function overrideSessionCheckForTests(check?: SessionCheck): void;
21
+ export {};
22
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/auth/session.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAYxD,qFAAqF;AACrF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CASjE;AAED,KAAK,YAAY,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AA8B1D,eAAO,MAAM,gBAAgB,GAAI,QAAQ,OAAO,KAAG,OAAO,CAAC,OAAO,CAAyB,CAAC;AAE5F;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,CAAC,EAAE,YAAY,GAAG,IAAI,CAEvE"}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isCurrentSession = exports.USER_SESSIONS_COLLECTION = void 0;
7
+ exports.isLegacySessionlessToken = isLegacySessionlessToken;
8
+ exports.overrideSessionCheckForTests = overrideSessionCheckForTests;
9
+ const mongoose_1 = __importDefault(require("mongoose"));
10
+ /**
11
+ * One document per signed-in device, owned and written by auth-service; every
12
+ * other service only reads it here. A session token names its document in the
13
+ * `sid` claim, and the token is only as good as that document: deleting it
14
+ * (sign-out, "sign out this device") ends the session on its next request, in
15
+ * every service, and its `expiresAt` (slid forward by auth-service while the
16
+ * device is in use, never past a hard cap from sign-in) is what idle expiry
17
+ * actually means.
18
+ */
19
+ exports.USER_SESSIONS_COLLECTION = 'user_sessions';
20
+ /**
21
+ * Tokens minted before sessions existed carry no `sid` and lived 24 hours.
22
+ * auth-service upgrades one to a session the next time `/auth/verify` sees it,
23
+ * so this window closes by itself a day after the sessions release; the branch
24
+ * accepting them can be deleted after that.
25
+ */
26
+ const LEGACY_TOKEN_MAX_LIFETIME_SECONDS = 24 * 60 * 60;
27
+ const OBJECT_ID = /^[a-f\d]{24}$/i;
28
+ /** True for a pre-sessions token: no `sid`, and the old 24-hour lifetime at most. */
29
+ function isLegacySessionlessToken(claims) {
30
+ if (!claims || typeof claims !== 'object')
31
+ return false;
32
+ const { sid, iat, exp } = claims;
33
+ return (sid === undefined &&
34
+ Number.isSafeInteger(iat) &&
35
+ Number.isSafeInteger(exp) &&
36
+ exp - iat <= LEGACY_TOKEN_MAX_LIFETIME_SECONDS);
37
+ }
38
+ /**
39
+ * Whether a verified session token still stands: for a token with a `sid`,
40
+ * its session belongs to the token's user and has neither been revoked nor
41
+ * expired. Read on the primary, uncached, so a revocation holds from the next
42
+ * request.
43
+ */
44
+ const checkSessionInDatabase = async (claims) => {
45
+ if (!claims || typeof claims !== 'object')
46
+ return false;
47
+ const { id, sid } = claims;
48
+ if (sid === undefined)
49
+ return isLegacySessionlessToken(claims);
50
+ if (typeof sid !== 'string' || !OBJECT_ID.test(sid))
51
+ return false;
52
+ if (typeof id !== 'string' || !OBJECT_ID.test(id))
53
+ return false;
54
+ // Refuse rather than queue an authentication query while the database is down.
55
+ if (mongoose_1.default.connection.readyState !== 1)
56
+ return false;
57
+ const session = await mongoose_1.default.connection.collection(exports.USER_SESSIONS_COLLECTION).findOne({
58
+ _id: new mongoose_1.default.Types.ObjectId(sid),
59
+ userId: new mongoose_1.default.Types.ObjectId(id),
60
+ expiresAt: { $gt: new Date() }
61
+ }, { projection: { _id: 1 }, readPreference: 'primary', maxTimeMS: 3000 });
62
+ return session !== null;
63
+ };
64
+ let sessionCheck = checkSessionInDatabase;
65
+ const isCurrentSession = (claims) => sessionCheck(claims);
66
+ exports.isCurrentSession = isCurrentSession;
67
+ /**
68
+ * Replaces the check, for tests only; pass nothing to restore the real one.
69
+ * Not exported from the package index: service suites reach it through the
70
+ * `@tumbaland/backend-core/testing` entry point (see testing.js).
71
+ */
72
+ function overrideSessionCheckForTests(check) {
73
+ sessionCheck = check ?? checkSessionInDatabase;
74
+ }
75
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/auth/session.ts"],"names":[],"mappings":";;;;;;AAwBA,4DASC;AAuCD,oEAEC;AA1ED,wDAAgC;AAEhC;;;;;;;;GAQG;AACU,QAAA,wBAAwB,GAAG,eAAe,CAAC;AAExD;;;;;GAKG;AACH,MAAM,iCAAiC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAEvD,MAAM,SAAS,GAAG,gBAAgB,CAAC;AAEnC,qFAAqF;AACrF,SAAgB,wBAAwB,CAAC,MAAe;IACtD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxD,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,MAAyD,CAAC;IACpF,OAAO,CACL,GAAG,KAAK,SAAS;QACjB,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC;QACzB,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC;QACxB,GAAc,GAAI,GAAc,IAAI,iCAAiC,CACvE,CAAC;AACJ,CAAC;AAID;;;;;GAKG;AACH,MAAM,sBAAsB,GAAiB,KAAK,EAAE,MAAM,EAAE,EAAE;IAC5D,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxD,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,MAAyC,CAAC;IAC9D,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAC/D,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAClE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IAChE,+EAA+E;IAC/E,IAAI,kBAAQ,CAAC,UAAU,CAAC,UAAU,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEvD,MAAM,OAAO,GAAG,MAAM,kBAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,gCAAwB,CAAC,CAAC,OAAO,CACpF;QACE,GAAG,EAAE,IAAI,kBAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QACrC,MAAM,EAAE,IAAI,kBAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvC,SAAS,EAAE,EAAE,GAAG,EAAE,IAAI,IAAI,EAAE,EAAE;KAC/B,EACD,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,CACvE,CAAC;IACF,OAAO,OAAO,KAAK,IAAI,CAAC;AAC1B,CAAC,CAAC;AAEF,IAAI,YAAY,GAAiB,sBAAsB,CAAC;AAEjD,MAAM,gBAAgB,GAAG,CAAC,MAAe,EAAoB,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;AAA/E,QAAA,gBAAgB,oBAA+D;AAE5F;;;;GAIG;AACH,SAAgB,4BAA4B,CAAC,KAAoB;IAC/D,YAAY,GAAG,KAAK,IAAI,sBAAsB,CAAC;AACjD,CAAC"}
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export * from './apiKeys';
16
16
  export * from './oauth';
17
17
  export * from './audit';
18
18
  export { authenticateToken, optionalAuth } from './middleware/authMiddleware';
19
+ export { USER_SESSIONS_COLLECTION, isCurrentSession, isLegacySessionlessToken } from './auth/session';
19
20
  export { requireInternalServiceToken, internalServiceTokenBypass, allowUserOrInternalService, verifyInternalToken, INTERNAL_TOKEN_HEADER, SERVICE_ID_HEADER } from './middleware/internalServiceAuth';
20
21
  export { createCorsMiddleware } from './middleware/corsMiddleware';
21
22
  export { errorHandler } from './middleware/errorHandler';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC7K,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,cAAc,gBAAgB,CAAC;AAG/B,cAAc,UAAU,CAAC;AAGzB,cAAc,WAAW,CAAC;AAG1B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC9E,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC1G,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACnH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,UAAU,EACV,aAAa,EACb,kBAAkB,EACnB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC7K,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,cAAc,gBAAgB,CAAC;AAG/B,cAAc,UAAU,CAAC;AAGzB,cAAc,WAAW,CAAC;AAG1B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACtG,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC1G,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACnH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,UAAU,EACV,aAAa,EACb,kBAAkB,EACnB,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -18,8 +18,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.register = exports.businessMetrics = exports.databaseQueriesTotal = exports.databaseQueryDuration = exports.httpRequestsTotal = exports.httpRequestDuration = exports.metricsMiddleware = exports.canAccessResource = exports.buildAccessQuery = exports.sendError = exports.sendSuccess = exports.createApiResponse = exports.correlationMiddleware = exports.generateCorrelationId = exports.validate = exports.strictRateLimiter = exports.standardRateLimiter = exports.createRateLimiter = exports.securityHeaders = exports.simpleRequestLogger = exports.requestLoggerWithMetrics = exports.requestLogger = exports.errorHandler = exports.createCorsMiddleware = exports.SERVICE_ID_HEADER = exports.INTERNAL_TOKEN_HEADER = exports.verifyInternalToken = exports.allowUserOrInternalService = exports.internalServiceTokenBypass = exports.requireInternalServiceToken = exports.optionalAuth = exports.authenticateToken = exports.PaymentRequiredError = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = exports.requireEnv = exports.disconnectDB = exports.connectDB = exports.createHealthCheck = exports.metricsHandler = exports.healthLive = exports.healthCheck = exports.logger = exports.registerShutdown = exports.createBaseApp = void 0;
22
- exports.extractSpanContext = exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = exports.initTracer = void 0;
21
+ exports.databaseQueryDuration = exports.httpRequestsTotal = exports.httpRequestDuration = exports.metricsMiddleware = exports.canAccessResource = exports.buildAccessQuery = exports.sendError = exports.sendSuccess = exports.createApiResponse = exports.correlationMiddleware = exports.generateCorrelationId = exports.validate = exports.strictRateLimiter = exports.standardRateLimiter = exports.createRateLimiter = exports.securityHeaders = exports.simpleRequestLogger = exports.requestLoggerWithMetrics = exports.requestLogger = exports.errorHandler = exports.createCorsMiddleware = exports.SERVICE_ID_HEADER = exports.INTERNAL_TOKEN_HEADER = exports.verifyInternalToken = exports.allowUserOrInternalService = exports.internalServiceTokenBypass = exports.requireInternalServiceToken = exports.isLegacySessionlessToken = exports.isCurrentSession = exports.USER_SESSIONS_COLLECTION = exports.optionalAuth = exports.authenticateToken = exports.PaymentRequiredError = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = exports.requireEnv = exports.disconnectDB = exports.connectDB = exports.createHealthCheck = exports.metricsHandler = exports.healthLive = exports.healthCheck = exports.logger = exports.registerShutdown = exports.createBaseApp = void 0;
22
+ exports.extractSpanContext = exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = exports.initTracer = exports.register = exports.businessMetrics = exports.databaseQueriesTotal = void 0;
23
23
  // App bootstrap
24
24
  var createBaseApp_1 = require("./app/createBaseApp");
25
25
  Object.defineProperty(exports, "createBaseApp", { enumerable: true, get: function () { return createBaseApp_1.createBaseApp; } });
@@ -65,6 +65,10 @@ __exportStar(require("./audit"), exports);
65
65
  var authMiddleware_1 = require("./middleware/authMiddleware");
66
66
  Object.defineProperty(exports, "authenticateToken", { enumerable: true, get: function () { return authMiddleware_1.authenticateToken; } });
67
67
  Object.defineProperty(exports, "optionalAuth", { enumerable: true, get: function () { return authMiddleware_1.optionalAuth; } });
68
+ var session_1 = require("./auth/session");
69
+ Object.defineProperty(exports, "USER_SESSIONS_COLLECTION", { enumerable: true, get: function () { return session_1.USER_SESSIONS_COLLECTION; } });
70
+ Object.defineProperty(exports, "isCurrentSession", { enumerable: true, get: function () { return session_1.isCurrentSession; } });
71
+ Object.defineProperty(exports, "isLegacySessionlessToken", { enumerable: true, get: function () { return session_1.isLegacySessionlessToken; } });
68
72
  var internalServiceAuth_1 = require("./middleware/internalServiceAuth");
69
73
  Object.defineProperty(exports, "requireInternalServiceToken", { enumerable: true, get: function () { return internalServiceAuth_1.requireInternalServiceToken; } });
70
74
  Object.defineProperty(exports, "internalServiceTokenBypass", { enumerable: true, get: function () { return internalServiceAuth_1.internalServiceTokenBypass; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,qFAAqF;;;;;;;;;;;;;;;;;;;;;AAErF,gBAAgB;AAChB,qDAAoD;AAA3C,8GAAA,aAAa,OAAA;AAEtB,2CAAkD;AAAzC,4GAAA,gBAAgB,OAAA;AAGzB,UAAU;AACV,2CAAqD;AAA5C,iHAAA,OAAO,OAAU;AAE1B,gBAAgB;AAChB,8DAAoF;AAA3E,+GAAA,WAAW,OAAA;AAAE,8GAAA,UAAU,OAAA;AAAE,kHAAA,cAAc,OAAA;AAChD,gEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAG1B,WAAW;AACX,oDAAgE;AAAvD,uGAAA,SAAS,OAAA;AAAE,0GAAA,YAAY,OAAA;AAEhC,SAAS;AACT,oCAA0C;AAAjC,iGAAA,UAAU,OAAA;AAEnB,SAAS;AACT,gDAA6K;AAApK,sGAAA,SAAS,OAAA;AAAE,4GAAA,eAAe,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAAE,2GAAA,cAAc,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAGhJ,wDAAwD;AACxD,iDAA+B;AAE/B,0DAA0D;AAC1D,2CAAyB;AAEzB,+DAA+D;AAC/D,4CAA0B;AAE1B,qEAAqE;AACrE,0CAAwB;AACxB,0CAAwB;AAExB,aAAa;AACb,8DAA8E;AAArE,mHAAA,iBAAiB,OAAA;AAAE,8GAAA,YAAY,OAAA;AACxC,wEAO0C;AANxC,kIAAA,2BAA2B,OAAA;AAC3B,iIAAA,0BAA0B,OAAA;AAC1B,iIAAA,0BAA0B,OAAA;AAC1B,0HAAA,mBAAmB,OAAA;AACnB,4HAAA,qBAAqB,OAAA;AACrB,wHAAA,iBAAiB,OAAA;AAEnB,8DAAmE;AAA1D,sHAAA,oBAAoB,OAAA;AAC7B,0DAAyD;AAAhD,4GAAA,YAAY,OAAA;AACrB,4DAA0G;AAAjG,8GAAA,aAAa,OAAA;AAAE,yHAAA,wBAAwB,OAAA;AAAE,oHAAA,mBAAmB,OAAA;AACrE,kDAAmH;AAA1G,2GAAA,eAAe,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AAAE,+GAAA,mBAAmB,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AACnF,kDAAiD;AAAxC,oGAAA,QAAQ,OAAA;AAKjB,QAAQ;AACR,mDAAmF;AAA1E,oHAAA,qBAAqB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACrD,6CAA6E;AAApE,6GAAA,iBAAiB,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,qGAAA,SAAS,OAAA;AAElD,2DAA8E;AAArE,mHAAA,gBAAgB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAE5C,UAAU;AACV,qCAQmB;AAPjB,4GAAA,iBAAiB,OAAA;AACjB,8GAAA,mBAAmB,OAAA;AACnB,4GAAA,iBAAiB,OAAA;AACjB,gHAAA,qBAAqB,OAAA;AACrB,+GAAA,oBAAoB,OAAA;AACpB,0GAAA,eAAe,OAAA;AACf,mGAAA,QAAQ,OAAA;AAGV,UAAU;AACV,qCAUmB;AATjB,qGAAA,UAAU,OAAA;AACV,oGAAA,SAAS,OAAA;AACT,oGAAA,SAAS,OAAA;AACT,4GAAA,iBAAiB,OAAA;AACjB,0GAAA,eAAe,OAAA;AACf,oGAAA,SAAS,OAAA;AACT,qGAAA,UAAU,OAAA;AACV,wGAAA,aAAa,OAAA;AACb,6GAAA,kBAAkB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,qFAAqF;;;;;;;;;;;;;;;;;;;;;AAErF,gBAAgB;AAChB,qDAAoD;AAA3C,8GAAA,aAAa,OAAA;AAEtB,2CAAkD;AAAzC,4GAAA,gBAAgB,OAAA;AAGzB,UAAU;AACV,2CAAqD;AAA5C,iHAAA,OAAO,OAAU;AAE1B,gBAAgB;AAChB,8DAAoF;AAA3E,+GAAA,WAAW,OAAA;AAAE,8GAAA,UAAU,OAAA;AAAE,kHAAA,cAAc,OAAA;AAChD,gEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAG1B,WAAW;AACX,oDAAgE;AAAvD,uGAAA,SAAS,OAAA;AAAE,0GAAA,YAAY,OAAA;AAEhC,SAAS;AACT,oCAA0C;AAAjC,iGAAA,UAAU,OAAA;AAEnB,SAAS;AACT,gDAA6K;AAApK,sGAAA,SAAS,OAAA;AAAE,4GAAA,eAAe,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAAE,2GAAA,cAAc,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAGhJ,wDAAwD;AACxD,iDAA+B;AAE/B,0DAA0D;AAC1D,2CAAyB;AAEzB,+DAA+D;AAC/D,4CAA0B;AAE1B,qEAAqE;AACrE,0CAAwB;AACxB,0CAAwB;AAExB,aAAa;AACb,8DAA8E;AAArE,mHAAA,iBAAiB,OAAA;AAAE,8GAAA,YAAY,OAAA;AACxC,0CAAsG;AAA7F,mHAAA,wBAAwB,OAAA;AAAE,2GAAA,gBAAgB,OAAA;AAAE,mHAAA,wBAAwB,OAAA;AAC7E,wEAO0C;AANxC,kIAAA,2BAA2B,OAAA;AAC3B,iIAAA,0BAA0B,OAAA;AAC1B,iIAAA,0BAA0B,OAAA;AAC1B,0HAAA,mBAAmB,OAAA;AACnB,4HAAA,qBAAqB,OAAA;AACrB,wHAAA,iBAAiB,OAAA;AAEnB,8DAAmE;AAA1D,sHAAA,oBAAoB,OAAA;AAC7B,0DAAyD;AAAhD,4GAAA,YAAY,OAAA;AACrB,4DAA0G;AAAjG,8GAAA,aAAa,OAAA;AAAE,yHAAA,wBAAwB,OAAA;AAAE,oHAAA,mBAAmB,OAAA;AACrE,kDAAmH;AAA1G,2GAAA,eAAe,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AAAE,+GAAA,mBAAmB,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AACnF,kDAAiD;AAAxC,oGAAA,QAAQ,OAAA;AAKjB,QAAQ;AACR,mDAAmF;AAA1E,oHAAA,qBAAqB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACrD,6CAA6E;AAApE,6GAAA,iBAAiB,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,qGAAA,SAAS,OAAA;AAElD,2DAA8E;AAArE,mHAAA,gBAAgB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAE5C,UAAU;AACV,qCAQmB;AAPjB,4GAAA,iBAAiB,OAAA;AACjB,8GAAA,mBAAmB,OAAA;AACnB,4GAAA,iBAAiB,OAAA;AACjB,gHAAA,qBAAqB,OAAA;AACrB,+GAAA,oBAAoB,OAAA;AACpB,0GAAA,eAAe,OAAA;AACf,mGAAA,QAAQ,OAAA;AAGV,UAAU;AACV,qCAUmB;AATjB,qGAAA,UAAU,OAAA;AACV,oGAAA,SAAS,OAAA;AACT,oGAAA,SAAS,OAAA;AACT,4GAAA,iBAAiB,OAAA;AACjB,0GAAA,eAAe,OAAA;AACf,oGAAA,SAAS,OAAA;AACT,qGAAA,UAAU,OAAA;AACV,wGAAA,aAAa,OAAA;AACb,6GAAA,kBAAkB,OAAA"}
@@ -12,7 +12,7 @@ import { Request, Response, NextFunction } from 'express';
12
12
  * enforces both — so anything still guarded by this one is a person's to do,
13
13
  * and letting a token through would be handing an assistant the account.
14
14
  */
15
- export declare function authenticateToken(req: Request, res: Response, next: NextFunction): void;
15
+ export declare function authenticateToken(req: Request, res: Response, next: NextFunction): Promise<void>;
16
16
  /**
17
17
  * Attaches the user when a valid token is present, and never rejects.
18
18
  *
@@ -26,5 +26,5 @@ export declare function authenticateToken(req: Request, res: Response, next: Nex
26
26
  * around would break the landing page for exactly the people most likely to
27
27
  * have one.
28
28
  */
29
- export declare function optionalAuth(req: Request, _res: Response, next: NextFunction): void;
29
+ export declare function optionalAuth(req: Request, _res: Response, next: NextFunction): Promise<void>;
30
30
  //# sourceMappingURL=authMiddleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"authMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM1D;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CA0BvF;AACD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAmBnF"}
1
+ {"version":3,"file":"authMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAO1D;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCtG;AACD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBlG"}
@@ -8,6 +8,7 @@ exports.optionalAuth = optionalAuth;
8
8
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
9
  const env_1 = require("../config/env");
10
10
  const tokens_1 = require("../oauth/tokens");
11
+ const session_1 = require("../auth/session");
11
12
  /**
12
13
  * JWT authentication middleware
13
14
  * Verifies JWT token and attaches user info (and its embedded groups) to
@@ -21,7 +22,7 @@ const tokens_1 = require("../oauth/tokens");
21
22
  * enforces both — so anything still guarded by this one is a person's to do,
22
23
  * and letting a token through would be handing an assistant the account.
23
24
  */
24
- function authenticateToken(req, res, next) {
25
+ async function authenticateToken(req, res, next) {
25
26
  try {
26
27
  const JWT_SECRET = (0, env_1.requireEnv)('JWT_SECRET');
27
28
  const token = req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
@@ -37,14 +38,21 @@ function authenticateToken(req, res, next) {
37
38
  });
38
39
  return;
39
40
  }
41
+ // A signed-out or revoked device keeps a validly signed token; only its
42
+ // session record says it has ended.
43
+ if (!(await (0, session_1.isCurrentSession)(claims))) {
44
+ res.status(401).json({ success: false, message: 'Session has ended' });
45
+ return;
46
+ }
40
47
  const user = claims;
41
48
  req.user = user;
42
49
  req.userGroups = user.groups ?? [];
43
- next();
44
50
  }
45
51
  catch (error) {
46
52
  res.status(401).json({ success: false, message: 'Invalid or expired token' });
53
+ return;
47
54
  }
55
+ next();
48
56
  }
49
57
  /**
50
58
  * Attaches the user when a valid token is present, and never rejects.
@@ -59,7 +67,7 @@ function authenticateToken(req, res, next) {
59
67
  * around would break the landing page for exactly the people most likely to
60
68
  * have one.
61
69
  */
62
- function optionalAuth(req, _res, next) {
70
+ async function optionalAuth(req, _res, next) {
63
71
  try {
64
72
  const JWT_SECRET = (0, env_1.requireEnv)('JWT_SECRET');
65
73
  const token = req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
@@ -67,7 +75,8 @@ function optionalAuth(req, _res, next) {
67
75
  const claims = jsonwebtoken_1.default.verify(token, JWT_SECRET);
68
76
  // An access token is not a session; treated as none, exactly like an
69
77
  // expired cookie, so the route still answers with its public form.
70
- if (!(0, tokens_1.isAccessTokenClaims)(claims)) {
78
+ // An ended session is treated as none, like an expired cookie.
79
+ if (!(0, tokens_1.isAccessTokenClaims)(claims) && (await (0, session_1.isCurrentSession)(claims))) {
71
80
  const user = claims;
72
81
  req.user = user;
73
82
  req.userGroups = user.groups ?? [];
@@ -1 +1 @@
1
- {"version":3,"file":"authMiddleware.js","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":";;;;;AAmBA,8CA0BC;AAcD,oCAmBC;AA7ED,gEAA+B;AAC/B,uCAA2C;AAC3C,4CAAsD;AAGtD;;;;;;;;;;;;GAYG;AACH,SAAgB,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC/E,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAA,gBAAU,EAAC,YAAY,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7F,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QAC7C,IAAI,IAAA,4BAAmB,EAAC,MAAM,CAAC,EAAE,CAAC;YAChC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,2EAA2E;aACrF,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,MAAM,IAAI,GAAG,MAAqB,CAAC;QACnC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAChB,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAC;IAChF,CAAC;AACH,CAAC;AACD;;;;;;;;;;;;GAYG;AACH,SAAgB,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC3E,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAA,gBAAU,EAAC,YAAY,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7F,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,MAAM,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YAC7C,qEAAqE;YACrE,mEAAmE;YACnE,IAAI,CAAC,IAAA,4BAAmB,EAAC,MAAM,CAAC,EAAE,CAAC;gBACjC,MAAM,IAAI,GAAG,MAAqB,CAAC;gBACnC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;gBAChB,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oCAAoC;IACtC,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"authMiddleware.js","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":";;;;;AAoBA,8CAiCC;AAcD,oCAoBC;AAtFD,gEAA+B;AAC/B,uCAA2C;AAC3C,4CAAsD;AAEtD,6CAAmD;AAEnD;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACrF,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAA,gBAAU,EAAC,YAAY,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7F,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QAC7C,IAAI,IAAA,4BAAmB,EAAC,MAAM,CAAC,EAAE,CAAC;YAChC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,2EAA2E;aACrF,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,wEAAwE;QACxE,oCAAoC;QACpC,IAAI,CAAC,CAAC,MAAM,IAAA,0BAAgB,EAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YACtC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC,CAAC;YACvE,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,MAAqB,CAAC;QACnC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAChB,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IACrC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAC;QAC9E,OAAO;IACT,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC;AACD;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IACjF,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAA,gBAAU,EAAC,YAAY,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7F,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,MAAM,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YAC7C,qEAAqE;YACrE,mEAAmE;YACnE,+DAA+D;YAC/D,IAAI,CAAC,IAAA,4BAAmB,EAAC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAA,0BAAgB,EAAC,MAAM,CAAC,CAAC,EAAE,CAAC;gBACrE,MAAM,IAAI,GAAG,MAAqB,CAAC;gBACnC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;gBAChB,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oCAAoC;IACtC,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=allowAllSessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allowAllSessions.d.ts","sourceRoot":"","sources":["../../src/testing/allowAllSessions.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const session_1 = require("../auth/session");
4
+ /**
5
+ * Jest setup for service test suites: they sign session tokens without a
6
+ * session and run with no database, so every token's session counts as live.
7
+ * The session check itself is tested in backend-core (auth/session.test.ts).
8
+ *
9
+ * Loaded through the package's test-only entry point:
10
+ * setupFiles: ['@tumbaland/backend-core/testing']
11
+ */
12
+ (0, session_1.overrideSessionCheckForTests)(async () => true);
13
+ //# sourceMappingURL=allowAllSessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allowAllSessions.js","sourceRoot":"","sources":["../../src/testing/allowAllSessions.ts"],"names":[],"mappings":";;AAAA,6CAA+D;AAE/D;;;;;;;GAOG;AACH,IAAA,sCAA4B,EAAC,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC"}
@@ -6,6 +6,9 @@ export interface UserPayload {
6
6
  picture?: string;
7
7
  groups?: string[];
8
8
  roles?: string[];
9
+ tokenVersion?: number;
10
+ /** The auth-service session (user_sessions) this token belongs to; absent on pre-sessions tokens. */
11
+ sid?: string;
9
12
  }
10
13
  /**
11
14
  * Attaches `user`/`userGroups` to every Express `Request` across all
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/types/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,WAAW,CAAC;YACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;YACtB,aAAa,CAAC,EAAE,MAAM,CAAC;YACvB,IAAI,CAAC,EAAE,IAAI,CAAC;SACb;KACF;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/types/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qGAAqG;IACrG,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;GAUG;AACH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,WAAW,CAAC;YACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;YACtB,aAAa,CAAC,EAAE,MAAM,CAAC;YACvB,IAAI,CAAC,EAAE,IAAI,CAAC;SACb;KACF;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tumbaland/backend-core",
3
- "version": "1.41.0",
3
+ "version": "1.42.0",
4
4
  "description": "Core shared functionality for Tumbaland backend services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,6 +11,7 @@ import { verifyApiKey } from './service';
11
11
  import { mintAccessToken, type MintAccessTokenInput } from '../oauth/tokens';
12
12
  import { PERSONAL_TENANT } from './types';
13
13
  import { authenticateAgent, requireScope, denyApiKeys } from './middleware';
14
+ import { overrideSessionCheckForTests } from '../auth/session';
14
15
 
15
16
  const mockedVerify = verifyApiKey as jest.Mock;
16
17
  const ORIGINAL_ENV = process.env;
@@ -65,6 +66,21 @@ describe('authenticateAgent — no credentials', () => {
65
66
  });
66
67
 
67
68
  describe('authenticateAgent — session tokens', () => {
69
+ beforeEach(() => overrideSessionCheckForTests(async () => true));
70
+ afterAll(() => overrideSessionCheckForTests());
71
+
72
+ it('rejects a validly signed JWT whose session has ended', async () => {
73
+ overrideSessionCheckForTests(async () => false);
74
+ const token = jwt.sign({ id: 'u1', email: 'a@b.c', name: 'A', sid: 'a'.repeat(24) }, 'test-jwt-secret');
75
+ const res = mockRes();
76
+ const next = jest.fn();
77
+
78
+ await authenticateAgent()(req({ headers: { authorization: `Bearer ${token}` } }), res, next);
79
+
80
+ expect(res.status).toHaveBeenCalledWith(401);
81
+ expect(next).not.toHaveBeenCalled();
82
+ });
83
+
68
84
  it('accepts a valid JWT and grants the full session, with no apiKey context', async () => {
69
85
  const token = jwt.sign({ id: 'u1', email: 'a@b.c', name: 'A', groups: ['g1', 'g2'] }, 'test-jwt-secret');
70
86
  const request = req({ headers: { authorization: `Bearer ${token}` } });
@@ -16,6 +16,7 @@ import {
16
16
  type ApiKeyTenant,
17
17
  type Tenant
18
18
  } from './types';
19
+ import { isCurrentSession } from '../auth/session';
19
20
 
20
21
  /**
21
22
  * Request-scoped facts about the key a request arrived on. Absent on ordinary
@@ -223,6 +224,10 @@ export const authenticateAgent = (...requiredScopes: ApiKeyScope[]): RequestHand
223
224
  }
224
225
 
225
226
  if (!isAccessTokenClaims(claims)) {
227
+ if (!(await isCurrentSession(claims))) {
228
+ unauthorized(res);
229
+ return;
230
+ }
226
231
  const user = claims as UserPayload;
227
232
  req.user = user;
228
233
  req.userGroups = user.groups ?? [];
@@ -0,0 +1,89 @@
1
+ const connection = { readyState: 1, collection: jest.fn() };
2
+
3
+ jest.mock('mongoose', () => {
4
+ const actual = jest.requireActual('mongoose');
5
+ return { __esModule: true, default: { connection, Types: actual.Types } };
6
+ });
7
+
8
+ import mongoose from 'mongoose';
9
+ import {
10
+ USER_SESSIONS_COLLECTION,
11
+ isCurrentSession,
12
+ isLegacySessionlessToken,
13
+ overrideSessionCheckForTests
14
+ } from './session';
15
+
16
+ const USER_ID = 'b'.repeat(24);
17
+ const SESSION_ID = 'a'.repeat(24);
18
+ const now = Math.floor(Date.now() / 1000);
19
+
20
+ describe('isLegacySessionlessToken', () => {
21
+ it('accepts a pre-sessions token that lived 24 hours at most', () => {
22
+ expect(isLegacySessionlessToken({ iat: now, exp: now + 24 * 60 * 60 })).toBe(true);
23
+ });
24
+
25
+ it('refuses a session-less token that lives longer, or has no expiry', () => {
26
+ expect(isLegacySessionlessToken({ iat: now, exp: now + 24 * 60 * 60 + 1 })).toBe(false);
27
+ expect(isLegacySessionlessToken({ iat: now })).toBe(false);
28
+ });
29
+
30
+ it('is never true for a token that names a session', () => {
31
+ expect(isLegacySessionlessToken({ sid: SESSION_ID, iat: now, exp: now + 60 })).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe('isCurrentSession', () => {
36
+ const findOne = jest.fn();
37
+
38
+ beforeEach(() => {
39
+ overrideSessionCheckForTests();
40
+ findOne.mockReset();
41
+ connection.readyState = 1;
42
+ connection.collection.mockReset().mockReturnValue({ findOne });
43
+ });
44
+
45
+ it('accepts a live session that belongs to the token user', async () => {
46
+ findOne.mockResolvedValue({ _id: SESSION_ID });
47
+
48
+ await expect(isCurrentSession({ id: USER_ID, sid: SESSION_ID })).resolves.toBe(true);
49
+
50
+ expect(mongoose.connection.collection).toHaveBeenCalledWith(USER_SESSIONS_COLLECTION);
51
+ const [query, options] = findOne.mock.calls[0];
52
+ expect(query._id.toString()).toBe(SESSION_ID);
53
+ expect(query.userId.toString()).toBe(USER_ID);
54
+ expect(query.expiresAt.$gt).toBeInstanceOf(Date);
55
+ expect(options).toMatchObject({ readPreference: 'primary' });
56
+ });
57
+
58
+ it('refuses a revoked or expired session', async () => {
59
+ findOne.mockResolvedValue(null);
60
+
61
+ await expect(isCurrentSession({ id: USER_ID, sid: SESSION_ID })).resolves.toBe(false);
62
+ });
63
+
64
+ it('refuses while the database is unavailable rather than letting the token through', async () => {
65
+ connection.readyState = 0;
66
+
67
+ await expect(isCurrentSession({ id: USER_ID, sid: SESSION_ID })).resolves.toBe(false);
68
+ expect(findOne).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it.each([
72
+ [{ id: USER_ID, sid: 'not-an-id' }],
73
+ [{ id: 'not-an-id', sid: SESSION_ID }],
74
+ [{ id: USER_ID, sid: 42 }],
75
+ [null]
76
+ ])('refuses malformed claims %j without a lookup', async (claims) => {
77
+ await expect(isCurrentSession(claims)).resolves.toBe(false);
78
+ expect(findOne).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it('accepts a short-lived pre-sessions token without a lookup', async () => {
82
+ await expect(isCurrentSession({ id: USER_ID, iat: now, exp: now + 60 })).resolves.toBe(true);
83
+ expect(findOne).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it('refuses a long-lived token with no session', async () => {
87
+ await expect(isCurrentSession({ id: USER_ID, iat: now, exp: now + 90 * 24 * 60 * 60 })).resolves.toBe(false);
88
+ });
89
+ });
@@ -0,0 +1,75 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ /**
4
+ * One document per signed-in device, owned and written by auth-service; every
5
+ * other service only reads it here. A session token names its document in the
6
+ * `sid` claim, and the token is only as good as that document: deleting it
7
+ * (sign-out, "sign out this device") ends the session on its next request, in
8
+ * every service, and its `expiresAt` (slid forward by auth-service while the
9
+ * device is in use, never past a hard cap from sign-in) is what idle expiry
10
+ * actually means.
11
+ */
12
+ export const USER_SESSIONS_COLLECTION = 'user_sessions';
13
+
14
+ /**
15
+ * Tokens minted before sessions existed carry no `sid` and lived 24 hours.
16
+ * auth-service upgrades one to a session the next time `/auth/verify` sees it,
17
+ * so this window closes by itself a day after the sessions release; the branch
18
+ * accepting them can be deleted after that.
19
+ */
20
+ const LEGACY_TOKEN_MAX_LIFETIME_SECONDS = 24 * 60 * 60;
21
+
22
+ const OBJECT_ID = /^[a-f\d]{24}$/i;
23
+
24
+ /** True for a pre-sessions token: no `sid`, and the old 24-hour lifetime at most. */
25
+ export function isLegacySessionlessToken(claims: unknown): boolean {
26
+ if (!claims || typeof claims !== 'object') return false;
27
+ const { sid, iat, exp } = claims as { sid?: unknown; iat?: unknown; exp?: unknown };
28
+ return (
29
+ sid === undefined &&
30
+ Number.isSafeInteger(iat) &&
31
+ Number.isSafeInteger(exp) &&
32
+ (exp as number) - (iat as number) <= LEGACY_TOKEN_MAX_LIFETIME_SECONDS
33
+ );
34
+ }
35
+
36
+ type SessionCheck = (claims: unknown) => Promise<boolean>;
37
+
38
+ /**
39
+ * Whether a verified session token still stands: for a token with a `sid`,
40
+ * its session belongs to the token's user and has neither been revoked nor
41
+ * expired. Read on the primary, uncached, so a revocation holds from the next
42
+ * request.
43
+ */
44
+ const checkSessionInDatabase: SessionCheck = async (claims) => {
45
+ if (!claims || typeof claims !== 'object') return false;
46
+ const { id, sid } = claims as { id?: unknown; sid?: unknown };
47
+ if (sid === undefined) return isLegacySessionlessToken(claims);
48
+ if (typeof sid !== 'string' || !OBJECT_ID.test(sid)) return false;
49
+ if (typeof id !== 'string' || !OBJECT_ID.test(id)) return false;
50
+ // Refuse rather than queue an authentication query while the database is down.
51
+ if (mongoose.connection.readyState !== 1) return false;
52
+
53
+ const session = await mongoose.connection.collection(USER_SESSIONS_COLLECTION).findOne(
54
+ {
55
+ _id: new mongoose.Types.ObjectId(sid),
56
+ userId: new mongoose.Types.ObjectId(id),
57
+ expiresAt: { $gt: new Date() }
58
+ },
59
+ { projection: { _id: 1 }, readPreference: 'primary', maxTimeMS: 3000 }
60
+ );
61
+ return session !== null;
62
+ };
63
+
64
+ let sessionCheck: SessionCheck = checkSessionInDatabase;
65
+
66
+ export const isCurrentSession = (claims: unknown): Promise<boolean> => sessionCheck(claims);
67
+
68
+ /**
69
+ * Replaces the check, for tests only; pass nothing to restore the real one.
70
+ * Not exported from the package index: service suites reach it through the
71
+ * `@tumbaland/backend-core/testing` entry point (see testing.js).
72
+ */
73
+ export function overrideSessionCheckForTests(check?: SessionCheck): void {
74
+ sessionCheck = check ?? checkSessionInDatabase;
75
+ }
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ export * from './audit';
39
39
 
40
40
  // Middleware
41
41
  export { authenticateToken, optionalAuth } from './middleware/authMiddleware';
42
+ export { USER_SESSIONS_COLLECTION, isCurrentSession, isLegacySessionlessToken } from './auth/session';
42
43
  export {
43
44
  requireInternalServiceToken,
44
45
  internalServiceTokenBypass,
@@ -4,6 +4,11 @@ import { authenticateToken, optionalAuth } from './authMiddleware';
4
4
  import { PERSONAL_TENANT } from '../apiKeys/types';
5
5
  import { mintAccessToken } from '../oauth/tokens';
6
6
  import { UserPayload } from '../types/auth';
7
+ import { overrideSessionCheckForTests } from '../auth/session';
8
+
9
+ // These tokens carry no session; the session check has its own tests below and in session.test.ts.
10
+ beforeEach(() => overrideSessionCheckForTests(async () => true));
11
+ afterAll(() => overrideSessionCheckForTests());
7
12
 
8
13
  function mockReq(overrides: Partial<Request> = {}): Request {
9
14
  return { cookies: {}, headers: {}, ...overrides } as unknown as Request;
@@ -31,75 +36,75 @@ describe('authenticateToken', () => {
31
36
 
32
37
  const payload: UserPayload = { id: '1', email: 'user@example.com', name: 'Test User' };
33
38
 
34
- it('attaches the decoded user and calls next for a valid cookie token', () => {
39
+ it('attaches the decoded user and calls next for a valid cookie token', async () => {
35
40
  const token = jwt.sign(payload, 'test-secret');
36
41
  const req = mockReq({ cookies: { access_token: token } });
37
42
  const res = mockRes();
38
43
 
39
- authenticateToken(req, res, next);
44
+ await authenticateToken(req, res, next);
40
45
 
41
46
  expect(next).toHaveBeenCalledTimes(1);
42
47
  expect(req.user).toMatchObject({ id: '1', email: 'user@example.com' });
43
48
  });
44
49
 
45
- it('attaches an empty userGroups array when the token has no groups claim', () => {
50
+ it('attaches an empty userGroups array when the token has no groups claim', async () => {
46
51
  const token = jwt.sign(payload, 'test-secret');
47
52
  const req = mockReq({ cookies: { access_token: token } });
48
53
  const res = mockRes();
49
54
 
50
- authenticateToken(req, res, next);
55
+ await authenticateToken(req, res, next);
51
56
 
52
57
  expect(req.userGroups).toEqual([]);
53
58
  });
54
59
 
55
- it('attaches userGroups from the token when present', () => {
60
+ it('attaches userGroups from the token when present', async () => {
56
61
  const token = jwt.sign({ ...payload, groups: ['group-1', 'group-2'] }, 'test-secret');
57
62
  const req = mockReq({ cookies: { access_token: token } });
58
63
  const res = mockRes();
59
64
 
60
- authenticateToken(req, res, next);
65
+ await authenticateToken(req, res, next);
61
66
 
62
67
  expect(req.userGroups).toEqual(['group-1', 'group-2']);
63
68
  });
64
69
 
65
- it('accepts a Bearer token from the Authorization header', () => {
70
+ it('accepts a Bearer token from the Authorization header', async () => {
66
71
  const token = jwt.sign(payload, 'test-secret');
67
72
  const req = mockReq({ headers: { authorization: `Bearer ${token}` } });
68
73
  const res = mockRes();
69
74
 
70
- authenticateToken(req, res, next);
75
+ await authenticateToken(req, res, next);
71
76
 
72
77
  expect(next).toHaveBeenCalledTimes(1);
73
78
  });
74
79
 
75
- it('returns 401 when no token is present', () => {
80
+ it('returns 401 when no token is present', async () => {
76
81
  const req = mockReq();
77
82
  const res = mockRes();
78
83
 
79
- authenticateToken(req, res, next);
84
+ await authenticateToken(req, res, next);
80
85
 
81
86
  expect(next).not.toHaveBeenCalled();
82
87
  expect(res.status).toHaveBeenCalledWith(401);
83
88
  expect(res.json).toHaveBeenCalledWith({ success: false, message: 'Access token required' });
84
89
  });
85
90
 
86
- it('returns 401 for an invalid token', () => {
91
+ it('returns 401 for an invalid token', async () => {
87
92
  const req = mockReq({ cookies: { access_token: 'not-a-real-token' } });
88
93
  const res = mockRes();
89
94
 
90
- authenticateToken(req, res, next);
95
+ await authenticateToken(req, res, next);
91
96
 
92
97
  expect(next).not.toHaveBeenCalled();
93
98
  expect(res.status).toHaveBeenCalledWith(401);
94
99
  expect(res.json).toHaveBeenCalledWith({ success: false, message: 'Invalid or expired token' });
95
100
  });
96
101
 
97
- it('returns 401 for a token signed with a different secret', () => {
102
+ it('returns 401 for a token signed with a different secret', async () => {
98
103
  const token = jwt.sign(payload, 'wrong-secret');
99
104
  const req = mockReq({ cookies: { access_token: token } });
100
105
  const res = mockRes();
101
106
 
102
- authenticateToken(req, res, next);
107
+ await authenticateToken(req, res, next);
103
108
 
104
109
  expect(res.status).toHaveBeenCalledWith(401);
105
110
  });
@@ -120,54 +125,54 @@ describe('optionalAuth', () => {
120
125
 
121
126
  const payload: UserPayload = { id: '1', email: 'user@example.com', name: 'Test User' };
122
127
 
123
- it('attaches the user when the token is valid', () => {
128
+ it('attaches the user when the token is valid', async () => {
124
129
  const req = mockReq({ cookies: { access_token: jwt.sign(payload, 'test-secret') } });
125
130
 
126
- optionalAuth(req, mockRes(), next);
131
+ await optionalAuth(req, mockRes(), next);
127
132
 
128
133
  expect(next).toHaveBeenCalledTimes(1);
129
134
  expect(req.user).toMatchObject({ email: 'user@example.com' });
130
135
  });
131
136
 
132
- it('reads a bearer token too', () => {
137
+ it('reads a bearer token too', async () => {
133
138
  const req = mockReq({ headers: { authorization: `Bearer ${jwt.sign(payload, 'test-secret')}` } });
134
139
 
135
- optionalAuth(req, mockRes(), next);
140
+ await optionalAuth(req, mockRes(), next);
136
141
 
137
142
  expect(req.user).toMatchObject({ email: 'user@example.com' });
138
143
  });
139
144
 
140
- it('continues anonymously when there is no token', () => {
145
+ it('continues anonymously when there is no token', async () => {
141
146
  const req = mockReq();
142
147
  const res = mockRes();
143
148
 
144
- optionalAuth(req, res, next);
149
+ await optionalAuth(req, res, next);
145
150
 
146
151
  expect(next).toHaveBeenCalledTimes(1);
147
152
  expect(req.user).toBeUndefined();
148
153
  expect(res.status).not.toHaveBeenCalled();
149
154
  });
150
155
 
151
- it('treats an expired or forged token as no token rather than as a 401', () => {
156
+ it('treats an expired or forged token as no token rather than as a 401', async () => {
152
157
  // The caller asked for public data. Refusing it because a stale cookie is
153
158
  // lying around would break the landing page for exactly the people most
154
159
  // likely to have one.
155
160
  const req = mockReq({ cookies: { access_token: jwt.sign(payload, 'wrong-secret') } });
156
161
  const res = mockRes();
157
162
 
158
- optionalAuth(req, res, next);
163
+ await optionalAuth(req, res, next);
159
164
 
160
165
  expect(next).toHaveBeenCalledTimes(1);
161
166
  expect(req.user).toBeUndefined();
162
167
  expect(res.status).not.toHaveBeenCalled();
163
168
  });
164
169
 
165
- it('continues anonymously when JWT_SECRET is not configured', () => {
170
+ it('continues anonymously when JWT_SECRET is not configured', async () => {
166
171
  delete process.env.JWT_SECRET;
167
172
  const req = mockReq({ cookies: { access_token: 'anything' } });
168
173
  const res = mockRes();
169
174
 
170
- optionalAuth(req, res, next);
175
+ await optionalAuth(req, res, next);
171
176
 
172
177
  expect(next).toHaveBeenCalledTimes(1);
173
178
  expect(res.status).not.toHaveBeenCalled();
@@ -200,28 +205,65 @@ describe('OAuth access tokens are not sessions', () => {
200
205
  clientName: 'Claude'
201
206
  }).accessToken;
202
207
 
203
- it('refuses one on a session-only route', () => {
208
+ it('refuses one on a session-only route', async () => {
204
209
  const res = mockRes();
205
210
  const next = jest.fn();
206
211
  const req = mockReq({ headers: { authorization: `Bearer ${accessToken()}` } });
207
212
 
208
213
  // It verifies — same secret — so nothing but the type check stands between
209
214
  // an assistant and every route that has no scopes to be held to.
210
- authenticateToken(req, res, next);
215
+ await authenticateToken(req, res, next);
211
216
 
212
217
  expect(res.status).toHaveBeenCalledWith(403);
213
218
  expect(next).not.toHaveBeenCalled();
214
219
  expect(req.user).toBeUndefined();
215
220
  });
216
221
 
217
- it('treats one as no session at all on an optional-auth route', () => {
222
+ it('treats one as no session at all on an optional-auth route', async () => {
218
223
  const next = jest.fn();
219
224
  const req = mockReq({ headers: { authorization: `Bearer ${accessToken()}` } });
220
225
 
221
- optionalAuth(req, mockRes(), next);
226
+ await optionalAuth(req, mockRes(), next);
222
227
 
223
228
  // The route still answers, in its public form — the same as a stale cookie.
224
229
  expect(next).toHaveBeenCalled();
225
230
  expect(req.user).toBeUndefined();
226
231
  });
227
232
  });
233
+
234
+ describe('ended sessions', () => {
235
+ const ORIGINAL_ENV = process.env;
236
+ const payload: UserPayload = { id: '1', email: 'user@example.com', name: 'Test User', sid: 'a'.repeat(24) };
237
+
238
+ beforeEach(() => {
239
+ process.env = { ...ORIGINAL_ENV, JWT_SECRET: 'test-secret' };
240
+ overrideSessionCheckForTests(async () => false);
241
+ });
242
+
243
+ afterAll(() => {
244
+ process.env = ORIGINAL_ENV;
245
+ });
246
+
247
+ it('authenticateToken refuses a validly signed token whose session has ended', async () => {
248
+ const req = mockReq({ cookies: { access_token: jwt.sign(payload, 'test-secret') } });
249
+ const res = mockRes();
250
+ const next = jest.fn();
251
+
252
+ await authenticateToken(req, res, next);
253
+
254
+ expect(res.status).toHaveBeenCalledWith(401);
255
+ expect(res.json).toHaveBeenCalledWith({ success: false, message: 'Session has ended' });
256
+ expect(next).not.toHaveBeenCalled();
257
+ expect(req.user).toBeUndefined();
258
+ });
259
+
260
+ it('optionalAuth treats an ended session as no session', async () => {
261
+ const req = mockReq({ cookies: { access_token: jwt.sign(payload, 'test-secret') } });
262
+ const next = jest.fn();
263
+
264
+ await optionalAuth(req, mockRes(), next);
265
+
266
+ expect(next).toHaveBeenCalledTimes(1);
267
+ expect(req.user).toBeUndefined();
268
+ });
269
+ });
@@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken';
3
3
  import { requireEnv } from '../config/env';
4
4
  import { isAccessTokenClaims } from '../oauth/tokens';
5
5
  import { UserPayload } from '../types/auth';
6
+ import { isCurrentSession } from '../auth/session';
6
7
 
7
8
  /**
8
9
  * JWT authentication middleware
@@ -17,7 +18,7 @@ import { UserPayload } from '../types/auth';
17
18
  * enforces both — so anything still guarded by this one is a person's to do,
18
19
  * and letting a token through would be handing an assistant the account.
19
20
  */
20
- export function authenticateToken(req: Request, res: Response, next: NextFunction): void {
21
+ export async function authenticateToken(req: Request, res: Response, next: NextFunction): Promise<void> {
21
22
  try {
22
23
  const JWT_SECRET = requireEnv('JWT_SECRET');
23
24
  const token = req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
@@ -36,13 +37,20 @@ export function authenticateToken(req: Request, res: Response, next: NextFunctio
36
37
  return;
37
38
  }
38
39
 
40
+ // A signed-out or revoked device keeps a validly signed token; only its
41
+ // session record says it has ended.
42
+ if (!(await isCurrentSession(claims))) {
43
+ res.status(401).json({ success: false, message: 'Session has ended' });
44
+ return;
45
+ }
39
46
  const user = claims as UserPayload;
40
47
  req.user = user;
41
48
  req.userGroups = user.groups ?? [];
42
- next();
43
49
  } catch (error) {
44
50
  res.status(401).json({ success: false, message: 'Invalid or expired token' });
51
+ return;
45
52
  }
53
+ next();
46
54
  }
47
55
  /**
48
56
  * Attaches the user when a valid token is present, and never rejects.
@@ -57,7 +65,7 @@ export function authenticateToken(req: Request, res: Response, next: NextFunctio
57
65
  * around would break the landing page for exactly the people most likely to
58
66
  * have one.
59
67
  */
60
- export function optionalAuth(req: Request, _res: Response, next: NextFunction): void {
68
+ export async function optionalAuth(req: Request, _res: Response, next: NextFunction): Promise<void> {
61
69
  try {
62
70
  const JWT_SECRET = requireEnv('JWT_SECRET');
63
71
  const token = req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
@@ -66,7 +74,8 @@ export function optionalAuth(req: Request, _res: Response, next: NextFunction):
66
74
  const claims = jwt.verify(token, JWT_SECRET);
67
75
  // An access token is not a session; treated as none, exactly like an
68
76
  // expired cookie, so the route still answers with its public form.
69
- if (!isAccessTokenClaims(claims)) {
77
+ // An ended session is treated as none, like an expired cookie.
78
+ if (!isAccessTokenClaims(claims) && (await isCurrentSession(claims))) {
70
79
  const user = claims as UserPayload;
71
80
  req.user = user;
72
81
  req.userGroups = user.groups ?? [];
@@ -0,0 +1,11 @@
1
+ import { overrideSessionCheckForTests } from '../auth/session';
2
+
3
+ /**
4
+ * Jest setup for service test suites: they sign session tokens without a
5
+ * session and run with no database, so every token's session counts as live.
6
+ * The session check itself is tested in backend-core (auth/session.test.ts).
7
+ *
8
+ * Loaded through the package's test-only entry point:
9
+ * setupFiles: ['@tumbaland/backend-core/testing']
10
+ */
11
+ overrideSessionCheckForTests(async () => true);
package/src/types/auth.ts CHANGED
@@ -7,6 +7,9 @@ export interface UserPayload {
7
7
  picture?: string;
8
8
  groups?: string[];
9
9
  roles?: string[];
10
+ tokenVersion?: number;
11
+ /** The auth-service session (user_sessions) this token belongs to; absent on pre-sessions tokens. */
12
+ sid?: string;
10
13
  }
11
14
 
12
15
  /**
package/testing.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Test-only entry point: `@tumbaland/backend-core/testing`.
3
+ *
4
+ * Loading it makes every session token's session count as live, for service
5
+ * test suites that sign tokens without a session and run with no database.
6
+ * Kept out of the package's main exports so production code never reaches it.
7
+ * The session check itself is tested in backend-core (src/auth/session.test.ts).
8
+ */
9
+ require('./dist/testing/allowAllSessions');