@tumbaland/backend-core 1.40.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.
- package/dist/apiKeys/middleware.d.ts.map +1 -1
- package/dist/apiKeys/middleware.js +5 -0
- package/dist/apiKeys/middleware.js.map +1 -1
- package/dist/auth/session.d.ts +22 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +75 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/authMiddleware.d.ts +2 -2
- package/dist/middleware/authMiddleware.d.ts.map +1 -1
- package/dist/middleware/authMiddleware.js +13 -4
- package/dist/middleware/authMiddleware.js.map +1 -1
- package/dist/testing/allowAllSessions.d.ts +2 -0
- package/dist/testing/allowAllSessions.d.ts.map +1 -0
- package/dist/testing/allowAllSessions.js +13 -0
- package/dist/testing/allowAllSessions.js.map +1 -0
- package/dist/types/auth.d.ts +3 -0
- package/dist/types/auth.d.ts.map +1 -1
- package/package.json +13 -13
- package/src/apiKeys/middleware.test.ts +16 -0
- package/src/apiKeys/middleware.ts +5 -0
- package/src/auth/session.test.ts +89 -0
- package/src/auth/session.ts +75 -0
- package/src/index.ts +1 -0
- package/src/middleware/authMiddleware.test.ts +70 -28
- package/src/middleware/authMiddleware.ts +13 -4
- package/src/testing/allowAllSessions.ts +11 -0
- package/src/types/auth.ts +3 -0
- package/testing.js +9 -0
|
@@ -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;
|
|
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":";;;;;;AACA,gEAA+B;AAC/B,uCAA2C;AAC3C,+DAAuC;AACvC,8CAA8C;AAC9C,4CAA8E;AAE9E,qCAA2C;AAC3C,uCAAyC;AACzC,mCAQiB;
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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.
|
|
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;
|
|
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
|
-
|
|
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":";;;;;
|
|
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 @@
|
|
|
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"}
|
package/dist/types/auth.d.ts
CHANGED
|
@@ -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
|
package/dist/types/auth.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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",
|
|
@@ -34,29 +34,29 @@
|
|
|
34
34
|
"@types/express": "^5.0.6",
|
|
35
35
|
"@types/jsonwebtoken": "^9.0.10",
|
|
36
36
|
"@types/morgan": "^1.9.10",
|
|
37
|
-
"@types/node": "^26.1
|
|
38
|
-
"commit-and-tag-version": "^13.1
|
|
37
|
+
"@types/node": "^26.5.1",
|
|
38
|
+
"commit-and-tag-version": "^13.2.1",
|
|
39
39
|
"mongodb-memory-server": "^11.2.0",
|
|
40
40
|
"typescript": "^6.0.3"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@opentelemetry/api": "^1.9.1",
|
|
44
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
45
|
-
"@opentelemetry/resources": "^2.
|
|
46
|
-
"@opentelemetry/sdk-trace-node": "^2.
|
|
47
|
-
"@opentelemetry/semantic-conventions": "^1.
|
|
44
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
|
|
45
|
+
"@opentelemetry/resources": "^2.11.0",
|
|
46
|
+
"@opentelemetry/sdk-trace-node": "^2.11.0",
|
|
47
|
+
"@opentelemetry/semantic-conventions": "^1.43.0",
|
|
48
48
|
"cookie-parser": "^1.4.7",
|
|
49
49
|
"cors": "^2.8.6",
|
|
50
50
|
"express": "^5.2.1",
|
|
51
|
-
"express-rate-limit": "^8.
|
|
52
|
-
"helmet": "^8.
|
|
51
|
+
"express-rate-limit": "^8.7.0",
|
|
52
|
+
"helmet": "^8.3.0",
|
|
53
53
|
"jsonwebtoken": "^9.0.3",
|
|
54
|
-
"mongoose": "
|
|
55
|
-
"morgan": "^1.
|
|
54
|
+
"mongoose": "~9.9.5",
|
|
55
|
+
"morgan": "^1.12.1",
|
|
56
56
|
"prom-client": "^15.1.3",
|
|
57
|
-
"uuid": "^14.0.
|
|
57
|
+
"uuid": "^14.0.2",
|
|
58
58
|
"winston": "^3.19.0",
|
|
59
|
-
"zod": "^4.
|
|
59
|
+
"zod": "^4.6.5"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"express": "^5.2.1",
|
|
@@ -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
|
-
|
|
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
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');
|