@tumbaland/backend-core 1.35.0 → 1.37.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 +5 -3
- package/dist/apiKeys/middleware.d.ts.map +1 -1
- package/dist/apiKeys/middleware.js +75 -34
- package/dist/apiKeys/middleware.js.map +1 -1
- package/dist/middleware/authMiddleware.d.ts +7 -0
- package/dist/middleware/authMiddleware.d.ts.map +1 -1
- package/dist/middleware/authMiddleware.js +25 -4
- package/dist/middleware/authMiddleware.js.map +1 -1
- package/dist/oauth/index.d.ts +1 -1
- package/dist/oauth/index.d.ts.map +1 -1
- package/dist/oauth/index.js +3 -1
- package/dist/oauth/index.js.map +1 -1
- package/dist/oauth/service.d.ts +22 -1
- package/dist/oauth/service.d.ts.map +1 -1
- package/dist/oauth/service.js +34 -5
- package/dist/oauth/service.js.map +1 -1
- package/dist/oauth/tokens.d.ts +12 -3
- package/dist/oauth/tokens.d.ts.map +1 -1
- package/dist/oauth/tokens.js +17 -4
- package/dist/oauth/tokens.js.map +1 -1
- package/package.json +1 -1
- package/src/apiKeys/middleware.test.ts +108 -0
- package/src/apiKeys/middleware.ts +97 -37
- package/src/middleware/authMiddleware.test.ts +49 -0
- package/src/middleware/authMiddleware.ts +26 -4
- package/src/oauth/index.ts +7 -1
- package/src/oauth/service.test.ts +55 -0
- package/src/oauth/service.ts +41 -5
- package/src/oauth/tokens.ts +18 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RequestHandler } from 'express';
|
|
2
|
-
import type
|
|
2
|
+
import { type ApiKeyScope } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Request-scoped facts about the key a request arrived on. Absent on ordinary
|
|
5
5
|
* session requests, which is itself the signal a handler needs: `req.apiKey`
|
|
@@ -40,11 +40,13 @@ export declare const authenticateAgent: (...requiredScopes: ApiKeyScope[]) => Re
|
|
|
40
40
|
* door for lacking a scope half the router never uses.
|
|
41
41
|
*
|
|
42
42
|
* A session passes unconditionally: scopes narrow what software may do on a
|
|
43
|
-
* person's behalf, not what the person may do themselves.
|
|
43
|
+
* person's behalf, not what the person may do themselves. Everything that is
|
|
44
|
+
* not a session — an API key or an OAuth access token alike — arrives with
|
|
45
|
+
* `req.apiKey` set and is held to it.
|
|
44
46
|
*/
|
|
45
47
|
export declare const requireScope: (...requiredScopes: ApiKeyScope[]) => RequestHandler;
|
|
46
48
|
/**
|
|
47
|
-
* Refuse
|
|
49
|
+
* Refuse every non-session credential on a route that a session may still use.
|
|
48
50
|
*
|
|
49
51
|
* For the handful of operations that should stay a person's to perform — key
|
|
50
52
|
* management itself, most obviously, since a key that can mint keys is a key
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAQ1E,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,aAAa,CAAC;SACxB;KACF;CACF;AAgHD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cA+DlE,CAAC;AAEJ;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAiB7D,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,cASzB,CAAC"}
|
|
@@ -7,8 +7,10 @@ exports.denyApiKeys = exports.requireScope = exports.authenticateAgent = void 0;
|
|
|
7
7
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
8
|
const env_1 = require("../config/env");
|
|
9
9
|
const logger_1 = __importDefault(require("../logging/logger"));
|
|
10
|
+
const tokens_1 = require("../oauth/tokens");
|
|
10
11
|
const crypto_1 = require("./crypto");
|
|
11
12
|
const service_1 = require("./service");
|
|
13
|
+
const types_1 = require("./types");
|
|
12
14
|
/** Both auth paths read the token from the same two places. */
|
|
13
15
|
const extractToken = (req) => req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
|
|
14
16
|
const unauthorized = (res) => {
|
|
@@ -58,6 +60,39 @@ const applyTenantPin = (req, groupId) => {
|
|
|
58
60
|
}
|
|
59
61
|
return true;
|
|
60
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Admit software acting for a user, on the terms its credential carries.
|
|
65
|
+
*
|
|
66
|
+
* Shared by both non-session credentials on purpose. An API key and an OAuth
|
|
67
|
+
* access token differ entirely in how they are issued and verified, and not at
|
|
68
|
+
* all in what they mean once they are: a user, a set of scopes, and one tenant.
|
|
69
|
+
* Settling that in one place is what keeps the two from drifting into subtly
|
|
70
|
+
* different amounts of access.
|
|
71
|
+
*/
|
|
72
|
+
function admitAgent(req, res, next, requiredScopes, credential) {
|
|
73
|
+
const missing = requiredScopes.filter((scope) => !credential.scopes.includes(scope));
|
|
74
|
+
if (missing.length > 0) {
|
|
75
|
+
res.status(403).json({
|
|
76
|
+
success: false,
|
|
77
|
+
message: `Credential is missing required scope: ${missing.join(', ')}`
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!applyTenantPin(req, credential.groupId)) {
|
|
82
|
+
res.status(403).json({
|
|
83
|
+
success: false,
|
|
84
|
+
message: 'Credential is not permitted to act in the requested group'
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
req.user = { id: credential.userId, email: credential.email, name: credential.name };
|
|
89
|
+
// Only the pinned group, never the owner's full membership: this is what
|
|
90
|
+
// stops a credential reaching a group it was not issued for through any
|
|
91
|
+
// handler that consults `userGroups` instead of the `groupId` parameter.
|
|
92
|
+
req.userGroups = credential.groupId ? [credential.groupId] : [];
|
|
93
|
+
req.apiKey = { keyId: credential.credentialId, scopes: credential.scopes, groupId: credential.groupId };
|
|
94
|
+
next();
|
|
95
|
+
}
|
|
61
96
|
/**
|
|
62
97
|
* Authenticate a request that software may legitimately be making.
|
|
63
98
|
*
|
|
@@ -77,15 +112,39 @@ const authenticateAgent = (...requiredScopes) => async function authenticateAgen
|
|
|
77
112
|
return;
|
|
78
113
|
}
|
|
79
114
|
if (!(0, crypto_1.looksLikeApiKey)(token)) {
|
|
115
|
+
let claims;
|
|
80
116
|
try {
|
|
81
|
-
|
|
82
|
-
req.user = user;
|
|
83
|
-
req.userGroups = user.groups ?? [];
|
|
84
|
-
next();
|
|
117
|
+
claims = jsonwebtoken_1.default.verify(token, (0, env_1.requireEnv)('JWT_SECRET'));
|
|
85
118
|
}
|
|
86
119
|
catch {
|
|
87
120
|
unauthorized(res);
|
|
121
|
+
return;
|
|
88
122
|
}
|
|
123
|
+
if (!(0, tokens_1.isAccessTokenClaims)(claims)) {
|
|
124
|
+
const user = claims;
|
|
125
|
+
req.user = user;
|
|
126
|
+
req.userGroups = user.groups ?? [];
|
|
127
|
+
next();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// An OAuth access token: signed with the same secret as a session and
|
|
131
|
+
// arriving in the same header, but nothing like one. It names its user in
|
|
132
|
+
// `sub`, carries scopes, and is pinned to a tenant — so it is admitted
|
|
133
|
+
// the way a key is, not the way a person is.
|
|
134
|
+
//
|
|
135
|
+
// Reading it as a session was the original bug and it failed quietly: the
|
|
136
|
+
// claims have no `id` and no `groups`, so `req.user.id` arrived as
|
|
137
|
+
// `undefined`, scoped writes were refused as inaccessible, and a read
|
|
138
|
+
// whose scope collapsed to nothing fell through to "no user context" and
|
|
139
|
+
// answered from the whole collection.
|
|
140
|
+
admitAgent(req, res, next, requiredScopes, {
|
|
141
|
+
credentialId: claims.jti,
|
|
142
|
+
userId: claims.sub,
|
|
143
|
+
email: claims.email ?? '',
|
|
144
|
+
name: claims.name ?? '',
|
|
145
|
+
scopes: (claims.scope ?? '').split(' ').filter(types_1.isApiKeyScope),
|
|
146
|
+
groupId: claims.groupId ?? null
|
|
147
|
+
});
|
|
89
148
|
return;
|
|
90
149
|
}
|
|
91
150
|
const result = await (0, service_1.verifyApiKey)(token);
|
|
@@ -96,34 +155,14 @@ const authenticateAgent = (...requiredScopes) => async function authenticateAgen
|
|
|
96
155
|
unauthorized(res);
|
|
97
156
|
return;
|
|
98
157
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
res.status(403).json({
|
|
103
|
-
success: false,
|
|
104
|
-
message: `API key is missing required scope: ${missing.join(', ')}`
|
|
105
|
-
});
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const groupId = result.groupId ?? null;
|
|
109
|
-
if (!applyTenantPin(req, groupId)) {
|
|
110
|
-
res.status(403).json({
|
|
111
|
-
success: false,
|
|
112
|
-
message: 'API key is not permitted to act in the requested group'
|
|
113
|
-
});
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
req.user = {
|
|
117
|
-
id: result.userId,
|
|
158
|
+
admitAgent(req, res, next, requiredScopes, {
|
|
159
|
+
credentialId: result.keyId,
|
|
160
|
+
userId: result.userId,
|
|
118
161
|
email: result.userEmail ?? '',
|
|
119
|
-
name: result.userName ?? ''
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// that consults `userGroups` instead of the `groupId` parameter.
|
|
124
|
-
req.userGroups = groupId ? [groupId] : [];
|
|
125
|
-
req.apiKey = { keyId: result.keyId, scopes, groupId };
|
|
126
|
-
next();
|
|
162
|
+
name: result.userName ?? '',
|
|
163
|
+
scopes: result.scopes ?? [],
|
|
164
|
+
groupId: result.groupId ?? null
|
|
165
|
+
});
|
|
127
166
|
};
|
|
128
167
|
exports.authenticateAgent = authenticateAgent;
|
|
129
168
|
/**
|
|
@@ -135,7 +174,9 @@ exports.authenticateAgent = authenticateAgent;
|
|
|
135
174
|
* door for lacking a scope half the router never uses.
|
|
136
175
|
*
|
|
137
176
|
* A session passes unconditionally: scopes narrow what software may do on a
|
|
138
|
-
* person's behalf, not what the person may do themselves.
|
|
177
|
+
* person's behalf, not what the person may do themselves. Everything that is
|
|
178
|
+
* not a session — an API key or an OAuth access token alike — arrives with
|
|
179
|
+
* `req.apiKey` set and is held to it.
|
|
139
180
|
*/
|
|
140
181
|
const requireScope = (...requiredScopes) => function requireScopeHandler(req, res, next) {
|
|
141
182
|
if (!req.apiKey) {
|
|
@@ -146,7 +187,7 @@ const requireScope = (...requiredScopes) => function requireScopeHandler(req, re
|
|
|
146
187
|
if (missing.length > 0) {
|
|
147
188
|
res.status(403).json({
|
|
148
189
|
success: false,
|
|
149
|
-
message: `
|
|
190
|
+
message: `Credential is missing required scope: ${missing.join(', ')}`
|
|
150
191
|
});
|
|
151
192
|
return;
|
|
152
193
|
}
|
|
@@ -154,7 +195,7 @@ const requireScope = (...requiredScopes) => function requireScopeHandler(req, re
|
|
|
154
195
|
};
|
|
155
196
|
exports.requireScope = requireScope;
|
|
156
197
|
/**
|
|
157
|
-
* Refuse
|
|
198
|
+
* Refuse every non-session credential on a route that a session may still use.
|
|
158
199
|
*
|
|
159
200
|
* For the handful of operations that should stay a person's to perform — key
|
|
160
201
|
* management itself, most obviously, since a key that can mint keys is a key
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":";;;;;;AACA,gEAA+B;AAC/B,uCAA2C;AAC3C,+DAAuC;
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,13 @@ import { Request, Response, NextFunction } from 'express';
|
|
|
4
4
|
* Verifies JWT token and attaches user info (and its embedded groups) to
|
|
5
5
|
* the request — typed via the global `Express.Request` augmentation in
|
|
6
6
|
* `../types/auth`, no `(req as any)` cast needed.
|
|
7
|
+
*
|
|
8
|
+
* OAuth access tokens are refused outright. They are signed with the same
|
|
9
|
+
* secret, so they verify here, but they mean something this middleware has no
|
|
10
|
+
* way to honour: an assistant acting within scopes, pinned to one tenant.
|
|
11
|
+
* Routes that software may legitimately reach use `authenticateAgent`, which
|
|
12
|
+
* enforces both — so anything still guarded by this one is a person's to do,
|
|
13
|
+
* and letting a token through would be handing an assistant the account.
|
|
7
14
|
*/
|
|
8
15
|
export declare function authenticateToken(req: Request, res: Response, next: NextFunction): void;
|
|
9
16
|
/**
|
|
@@ -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;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"}
|
|
@@ -7,11 +7,19 @@ exports.authenticateToken = authenticateToken;
|
|
|
7
7
|
exports.optionalAuth = optionalAuth;
|
|
8
8
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
9
|
const env_1 = require("../config/env");
|
|
10
|
+
const tokens_1 = require("../oauth/tokens");
|
|
10
11
|
/**
|
|
11
12
|
* JWT authentication middleware
|
|
12
13
|
* Verifies JWT token and attaches user info (and its embedded groups) to
|
|
13
14
|
* the request — typed via the global `Express.Request` augmentation in
|
|
14
15
|
* `../types/auth`, no `(req as any)` cast needed.
|
|
16
|
+
*
|
|
17
|
+
* OAuth access tokens are refused outright. They are signed with the same
|
|
18
|
+
* secret, so they verify here, but they mean something this middleware has no
|
|
19
|
+
* way to honour: an assistant acting within scopes, pinned to one tenant.
|
|
20
|
+
* Routes that software may legitimately reach use `authenticateAgent`, which
|
|
21
|
+
* enforces both — so anything still guarded by this one is a person's to do,
|
|
22
|
+
* and letting a token through would be handing an assistant the account.
|
|
15
23
|
*/
|
|
16
24
|
function authenticateToken(req, res, next) {
|
|
17
25
|
try {
|
|
@@ -21,7 +29,15 @@ function authenticateToken(req, res, next) {
|
|
|
21
29
|
res.status(401).json({ success: false, message: 'Access token required' });
|
|
22
30
|
return;
|
|
23
31
|
}
|
|
24
|
-
const
|
|
32
|
+
const claims = jsonwebtoken_1.default.verify(token, JWT_SECRET);
|
|
33
|
+
if ((0, tokens_1.isAccessTokenClaims)(claims)) {
|
|
34
|
+
res.status(403).json({
|
|
35
|
+
success: false,
|
|
36
|
+
message: 'This operation requires an interactive session, not a connected assistant'
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const user = claims;
|
|
25
41
|
req.user = user;
|
|
26
42
|
req.userGroups = user.groups ?? [];
|
|
27
43
|
next();
|
|
@@ -48,9 +64,14 @@ function optionalAuth(req, _res, next) {
|
|
|
48
64
|
const JWT_SECRET = (0, env_1.requireEnv)('JWT_SECRET');
|
|
49
65
|
const token = req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
|
|
50
66
|
if (token) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
const claims = jsonwebtoken_1.default.verify(token, JWT_SECRET);
|
|
68
|
+
// An access token is not a session; treated as none, exactly like an
|
|
69
|
+
// expired cookie, so the route still answers with its public form.
|
|
70
|
+
if (!(0, tokens_1.isAccessTokenClaims)(claims)) {
|
|
71
|
+
const user = claims;
|
|
72
|
+
req.user = user;
|
|
73
|
+
req.userGroups = user.groups ?? [];
|
|
74
|
+
}
|
|
54
75
|
}
|
|
55
76
|
}
|
|
56
77
|
catch {
|
|
@@ -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":";;;;;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"}
|
package/dist/oauth/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { OAuthClient, AuthorizationCode, RefreshToken } from './models';
|
|
|
2
2
|
export type { IOAuthClient, IAuthorizationCode, IRefreshToken } from './models';
|
|
3
3
|
export { registerClient, findClient, isRegisteredRedirect, issueAuthorizationCode, redeemAuthorizationCode, issueRefreshToken, redeemRefreshToken, revokeRefreshTokensForUser, listConnections } from './service';
|
|
4
4
|
export type { RegisterClientInput, RegisteredClient, IssueCodeInput, CodeRedemption, CodeRejection, RefreshRedemption } from './service';
|
|
5
|
-
export { mintAccessToken, verifyAccessToken, parseScopes } from './tokens';
|
|
5
|
+
export { mintAccessToken, verifyAccessToken, parseScopes, isAccessTokenClaims, ACCESS_TOKEN_TYPE } from './tokens';
|
|
6
6
|
export type { AccessTokenClaims, MintAccessTokenInput, MintedAccessToken, AccessTokenVerification } from './tokens';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAChF,OAAO,EACL,cAAc,EACd,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,eAAe,EAChB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,aAAa,EACb,iBAAiB,EAClB,MAAM,WAAW,CAAC;AACnB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAChF,OAAO,EACL,cAAc,EACd,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,eAAe,EAChB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,aAAa,EACb,iBAAiB,EAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/oauth/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseScopes = exports.verifyAccessToken = exports.mintAccessToken = exports.listConnections = exports.revokeRefreshTokensForUser = exports.redeemRefreshToken = exports.issueRefreshToken = exports.redeemAuthorizationCode = exports.issueAuthorizationCode = exports.isRegisteredRedirect = exports.findClient = exports.registerClient = exports.RefreshToken = exports.AuthorizationCode = exports.OAuthClient = void 0;
|
|
3
|
+
exports.ACCESS_TOKEN_TYPE = exports.isAccessTokenClaims = exports.parseScopes = exports.verifyAccessToken = exports.mintAccessToken = exports.listConnections = exports.revokeRefreshTokensForUser = exports.redeemRefreshToken = exports.issueRefreshToken = exports.redeemAuthorizationCode = exports.issueAuthorizationCode = exports.isRegisteredRedirect = exports.findClient = exports.registerClient = exports.RefreshToken = exports.AuthorizationCode = exports.OAuthClient = void 0;
|
|
4
4
|
var models_1 = require("./models");
|
|
5
5
|
Object.defineProperty(exports, "OAuthClient", { enumerable: true, get: function () { return models_1.OAuthClient; } });
|
|
6
6
|
Object.defineProperty(exports, "AuthorizationCode", { enumerable: true, get: function () { return models_1.AuthorizationCode; } });
|
|
@@ -19,4 +19,6 @@ var tokens_1 = require("./tokens");
|
|
|
19
19
|
Object.defineProperty(exports, "mintAccessToken", { enumerable: true, get: function () { return tokens_1.mintAccessToken; } });
|
|
20
20
|
Object.defineProperty(exports, "verifyAccessToken", { enumerable: true, get: function () { return tokens_1.verifyAccessToken; } });
|
|
21
21
|
Object.defineProperty(exports, "parseScopes", { enumerable: true, get: function () { return tokens_1.parseScopes; } });
|
|
22
|
+
Object.defineProperty(exports, "isAccessTokenClaims", { enumerable: true, get: function () { return tokens_1.isAccessTokenClaims; } });
|
|
23
|
+
Object.defineProperty(exports, "ACCESS_TOKEN_TYPE", { enumerable: true, get: function () { return tokens_1.ACCESS_TOKEN_TYPE; } });
|
|
22
24
|
//# sourceMappingURL=index.js.map
|
package/dist/oauth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":";;;AAAA,mCAAwE;AAA/D,qGAAA,WAAW,OAAA;AAAE,2GAAA,iBAAiB,OAAA;AAAE,sGAAA,YAAY,OAAA;AAErD,qCAUmB;AATjB,yGAAA,cAAc,OAAA;AACd,qGAAA,UAAU,OAAA;AACV,+GAAA,oBAAoB,OAAA;AACpB,iHAAA,sBAAsB,OAAA;AACtB,kHAAA,uBAAuB,OAAA;AACvB,4GAAA,iBAAiB,OAAA;AACjB,6GAAA,kBAAkB,OAAA;AAClB,qHAAA,0BAA0B,OAAA;AAC1B,0GAAA,eAAe,OAAA;AAUjB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":";;;AAAA,mCAAwE;AAA/D,qGAAA,WAAW,OAAA;AAAE,2GAAA,iBAAiB,OAAA;AAAE,sGAAA,YAAY,OAAA;AAErD,qCAUmB;AATjB,yGAAA,cAAc,OAAA;AACd,qGAAA,UAAU,OAAA;AACV,+GAAA,oBAAoB,OAAA;AACpB,iHAAA,sBAAsB,OAAA;AACtB,kHAAA,uBAAuB,OAAA;AACvB,4GAAA,iBAAiB,OAAA;AACjB,6GAAA,kBAAkB,OAAA;AAClB,qHAAA,0BAA0B,OAAA;AAC1B,0GAAA,eAAe,OAAA;AAUjB,mCAMkB;AALhB,yGAAA,eAAe,OAAA;AACf,2GAAA,iBAAiB,OAAA;AACjB,qGAAA,WAAW,OAAA;AACX,6GAAA,mBAAmB,OAAA;AACnB,2GAAA,iBAAiB,OAAA"}
|
package/dist/oauth/service.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ export interface RegisteredClient {
|
|
|
9
9
|
clientName: string;
|
|
10
10
|
redirectUris: string[];
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Register a client, or hand back the one that is already registered.
|
|
14
|
+
*
|
|
15
|
+
* Clients re-register freely — the Claude app does it on every connect attempt
|
|
16
|
+
* — and minting a fresh id each time left a user staring at three identical
|
|
17
|
+
* "Claude" entries in their connected apps, only one of which any given
|
|
18
|
+
* disconnect would cut off.
|
|
19
|
+
*
|
|
20
|
+
* Matching on the exact redirect URIs is what makes this safe. These are public
|
|
21
|
+
* clients with no secret, so a `client_id` is an identifier rather than a
|
|
22
|
+
* credential; anything presenting the same callback URL *is* the same
|
|
23
|
+
* application, because that URL is what an authorization code gets delivered
|
|
24
|
+
* to. A different app cannot claim it without already controlling it.
|
|
25
|
+
*/
|
|
12
26
|
export declare const registerClient: (input: RegisterClientInput) => Promise<RegisteredClient>;
|
|
13
27
|
export declare const findClient: (clientId: string) => import("mongoose").Query<(import("mongoose").Document<unknown, {}, import("./models").IOAuthClient, {}, import("mongoose").DefaultSchemaOptions> & import("./models").IOAuthClient & Required<{
|
|
14
28
|
_id: import("mongoose").Types.ObjectId;
|
|
@@ -62,6 +76,13 @@ export declare const redeemAuthorizationCode: (code: string, clientId: string, r
|
|
|
62
76
|
export interface IssuedRefreshToken {
|
|
63
77
|
token: string;
|
|
64
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Start a grant, replacing any the same client already held for this user.
|
|
81
|
+
*
|
|
82
|
+
* Reconnecting is a re-authorization, not a second connection: without this,
|
|
83
|
+
* approving twice leaves two live grants, the newer scopes sitting beside the
|
|
84
|
+
* older ones, and a disconnect that revokes only one of them.
|
|
85
|
+
*/
|
|
65
86
|
export declare const issueRefreshToken: (input: {
|
|
66
87
|
clientId: string;
|
|
67
88
|
userId: string;
|
|
@@ -105,7 +126,7 @@ export declare const listConnections: (userId: string) => Promise<{
|
|
|
105
126
|
scopes: string[];
|
|
106
127
|
groupId: string | null;
|
|
107
128
|
createdAt: string;
|
|
108
|
-
|
|
129
|
+
lastRenewedAt: string | null;
|
|
109
130
|
revokedAt: string | null;
|
|
110
131
|
}[]>;
|
|
111
132
|
//# sourceMappingURL=service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/oauth/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,kBAAkB,EAA6B,MAAM,UAAU,CAAC;AAC5F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAkBpD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,GAAU,OAAO,mBAAmB,KAAG,OAAO,CAAC,gBAAgB,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/oauth/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,kBAAkB,EAA6B,MAAM,UAAU,CAAC;AAC5F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAkBpD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAU,OAAO,mBAAmB,KAAG,OAAO,CAAC,gBAAgB,CAoBzF,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,UAAU,MAAM;;;;;;;;;;;;sDAAsC,CAAC;AAElF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ;IAAE,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,KAAK,MAAM,KAAG,OACpD,CAAC;AAEpC,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,sBAAsB,GAAU,OAAO,cAAc,KAAG,OAAO,CAAC,MAAM,CAUlF,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,SAAS,GACT,cAAc,GACd,iBAAiB,GACjB,mBAAmB,GACnB,aAAa,CAAC;AAElB,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GAClC,MAAM,MAAM,EACZ,UAAU,MAAM,EAChB,aAAa,MAAM,EACnB,cAAc,MAAM,KACnB,OAAO,CAAC,cAAc,CAsBxB,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAU,OAAO;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAG,OAAO,CAAC,kBAAkB,CAS7B,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,MAAM,EACb,UAAU,MAAM,KACf,OAAO,CAAC,iBAAiB,CAsC3B,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,0BAA0B,GACrC,QAAQ,MAAM,EACd,WAAW,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,QAAQ,MAAM;;;;;;;;;IAiBnD,CAAC"}
|
package/dist/oauth/service.js
CHANGED
|
@@ -15,12 +15,31 @@ const constantTimeEquals = (a, b) => {
|
|
|
15
15
|
return false;
|
|
16
16
|
return (0, crypto_1.timingSafeEqual)(left, right);
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Register a client, or hand back the one that is already registered.
|
|
20
|
+
*
|
|
21
|
+
* Clients re-register freely — the Claude app does it on every connect attempt
|
|
22
|
+
* — and minting a fresh id each time left a user staring at three identical
|
|
23
|
+
* "Claude" entries in their connected apps, only one of which any given
|
|
24
|
+
* disconnect would cut off.
|
|
25
|
+
*
|
|
26
|
+
* Matching on the exact redirect URIs is what makes this safe. These are public
|
|
27
|
+
* clients with no secret, so a `client_id` is an identifier rather than a
|
|
28
|
+
* credential; anything presenting the same callback URL *is* the same
|
|
29
|
+
* application, because that URL is what an authorization code gets delivered
|
|
30
|
+
* to. A different app cannot claim it without already controlling it.
|
|
31
|
+
*/
|
|
18
32
|
const registerClient = async (input) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
redirectUris: input.redirectUris
|
|
33
|
+
const redirectUris = [...input.redirectUris].sort();
|
|
34
|
+
const existing = await models_1.OAuthClient.findOne({
|
|
35
|
+
redirectUris: { $size: redirectUris.length, $all: redirectUris }
|
|
23
36
|
});
|
|
37
|
+
const client = existing ??
|
|
38
|
+
(await models_1.OAuthClient.create({
|
|
39
|
+
clientId: `tmb-client-${(0, crypto_1.randomBytes)(16).toString('hex')}`,
|
|
40
|
+
clientName: input.clientName.slice(0, 200),
|
|
41
|
+
redirectUris
|
|
42
|
+
}));
|
|
24
43
|
return {
|
|
25
44
|
clientId: client.clientId,
|
|
26
45
|
clientName: client.clientName,
|
|
@@ -76,7 +95,15 @@ const redeemAuthorizationCode = async (code, clientId, redirectUri, codeVerifier
|
|
|
76
95
|
return { ok: true, code: record };
|
|
77
96
|
};
|
|
78
97
|
exports.redeemAuthorizationCode = redeemAuthorizationCode;
|
|
98
|
+
/**
|
|
99
|
+
* Start a grant, replacing any the same client already held for this user.
|
|
100
|
+
*
|
|
101
|
+
* Reconnecting is a re-authorization, not a second connection: without this,
|
|
102
|
+
* approving twice leaves two live grants, the newer scopes sitting beside the
|
|
103
|
+
* older ones, and a disconnect that revokes only one of them.
|
|
104
|
+
*/
|
|
79
105
|
const issueRefreshToken = async (input) => {
|
|
106
|
+
await models_1.RefreshToken.updateMany({ userId: input.userId, clientId: input.clientId, revokedAt: { $exists: false } }, { $set: { revokedAt: new Date() } });
|
|
80
107
|
const token = (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
81
108
|
await models_1.RefreshToken.create({ ...input, tokenHash: sha256(token) });
|
|
82
109
|
return { token };
|
|
@@ -151,7 +178,9 @@ const listConnections = async (userId) => {
|
|
|
151
178
|
scopes: token.scopes,
|
|
152
179
|
groupId: token.groupId ?? null,
|
|
153
180
|
createdAt: (token.grantedAt ?? token.createdAt).toISOString(),
|
|
154
|
-
|
|
181
|
+
// Set when the refresh token is exchanged, not when a tool runs — access
|
|
182
|
+
// tokens are validated statelessly, so the server never sees ordinary use.
|
|
183
|
+
lastRenewedAt: token.lastUsedAt?.toISOString() ?? null,
|
|
155
184
|
revokedAt: token.revokedAt?.toISOString() ?? null
|
|
156
185
|
}));
|
|
157
186
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/oauth/service.ts"],"names":[],"mappings":";;;AAAA,mCAAkE;AAClE,qCAA4F;AAG5F,gFAAgF;AAChF,MAAM,WAAW,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9B,MAAM,MAAM,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAE3F,4EAA4E;AAC5E,MAAM,IAAI,GAAG,CAAC,QAAgB,EAAU,EAAE,CACxC,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAE5D,MAAM,kBAAkB,GAAG,CAAC,CAAS,EAAE,CAAS,EAAW,EAAE;IAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC/C,OAAO,IAAA,wBAAe,EAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/oauth/service.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth/tokens.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type ApiKeyScope } from '../apiKeys/types';
|
|
|
10
10
|
* token, which is the thing that actually persists.
|
|
11
11
|
*/
|
|
12
12
|
/** Marks a token as issued by the OAuth flow, for the MCP resource specifically. */
|
|
13
|
-
declare const
|
|
13
|
+
export declare const ACCESS_TOKEN_TYPE = "mcp_access";
|
|
14
14
|
export interface AccessTokenClaims {
|
|
15
15
|
/** the user the assistant is acting for */
|
|
16
16
|
sub: string;
|
|
@@ -22,7 +22,7 @@ export interface AccessTokenClaims {
|
|
|
22
22
|
groupId: string | null;
|
|
23
23
|
email: string;
|
|
24
24
|
name: string;
|
|
25
|
-
typ: typeof
|
|
25
|
+
typ: typeof ACCESS_TOKEN_TYPE;
|
|
26
26
|
jti: string;
|
|
27
27
|
exp: number;
|
|
28
28
|
iat: number;
|
|
@@ -63,7 +63,16 @@ export interface AccessTokenVerification {
|
|
|
63
63
|
* pin — cannot be presented as an access token and quietly get everything.
|
|
64
64
|
*/
|
|
65
65
|
export declare const verifyAccessToken: (token: string, expectedAudience: string) => AccessTokenVerification;
|
|
66
|
+
/**
|
|
67
|
+
* Tell an OAuth access token apart from an ordinary session JWT.
|
|
68
|
+
*
|
|
69
|
+
* They are signed with the same secret and arrive in the same header, so
|
|
70
|
+
* anything holding one has to ask which it got. Getting this wrong is not a
|
|
71
|
+
* subtle failure: read as a session, an access token has no `id` and no
|
|
72
|
+
* `groups`, so `req.user.id` lands as `undefined` and every scoped query goes
|
|
73
|
+
* out unbounded.
|
|
74
|
+
*/
|
|
75
|
+
export declare const isAccessTokenClaims: (claims: unknown) => claims is AccessTokenClaims;
|
|
66
76
|
/** Parse a space-separated `scope` parameter, dropping anything we do not define. */
|
|
67
77
|
export declare const parseScopes: (scope: unknown) => ApiKeyScope[];
|
|
68
|
-
export {};
|
|
69
78
|
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/oauth/tokens.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEnE;;;;;;;;;GASG;AAEH,oFAAoF;AACpF,
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/oauth/tokens.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEnE;;;;;;;;;GASG;AAEH,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAK9C,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,OAAO,iBAAiB,CAAC;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,eAAe,GAAI,OAAO,oBAAoB,KAAG,iBAoB7D,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,CAAC,EAAE,WAAW,GAAG,SAAS,GAAG,gBAAgB,GAAG,YAAY,GAAG,eAAe,CAAC;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAC5B,OAAO,MAAM,EACb,kBAAkB,MAAM,KACvB,uBAqBF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,OAAO,KAAG,MAAM,IAAI,iBAGP,CAAC;AAE1D,qFAAqF;AACrF,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,WAAW,EACmB,CAAC"}
|
package/dist/oauth/tokens.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.parseScopes = exports.verifyAccessToken = exports.mintAccessToken = void 0;
|
|
6
|
+
exports.parseScopes = exports.isAccessTokenClaims = exports.verifyAccessToken = exports.mintAccessToken = exports.ACCESS_TOKEN_TYPE = void 0;
|
|
7
7
|
const crypto_1 = require("crypto");
|
|
8
8
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
9
|
const env_1 = require("../config/env");
|
|
@@ -19,7 +19,7 @@ const types_1 = require("../apiKeys/types");
|
|
|
19
19
|
* token, which is the thing that actually persists.
|
|
20
20
|
*/
|
|
21
21
|
/** Marks a token as issued by the OAuth flow, for the MCP resource specifically. */
|
|
22
|
-
|
|
22
|
+
exports.ACCESS_TOKEN_TYPE = 'mcp_access';
|
|
23
23
|
/** Short enough that a leaked token is a small window, long enough to be usable. */
|
|
24
24
|
const ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
25
25
|
const mintAccessToken = (input) => {
|
|
@@ -32,7 +32,7 @@ const mintAccessToken = (input) => {
|
|
|
32
32
|
groupId: input.groupId,
|
|
33
33
|
email: input.email,
|
|
34
34
|
name: input.name,
|
|
35
|
-
typ:
|
|
35
|
+
typ: exports.ACCESS_TOKEN_TYPE,
|
|
36
36
|
jti: (0, crypto_1.randomUUID)()
|
|
37
37
|
}, (0, env_1.requireEnv)('JWT_SECRET'), { expiresIn: ACCESS_TOKEN_TTL_SECONDS });
|
|
38
38
|
return { accessToken, expiresIn: ACCESS_TOKEN_TTL_SECONDS, scope };
|
|
@@ -57,7 +57,7 @@ const verifyAccessToken = (token, expectedAudience) => {
|
|
|
57
57
|
const expired = error?.name === 'TokenExpiredError';
|
|
58
58
|
return { ok: false, rejection: expired ? 'expired' : 'bad-signature' };
|
|
59
59
|
}
|
|
60
|
-
if (
|
|
60
|
+
if (!(0, exports.isAccessTokenClaims)(claims))
|
|
61
61
|
return { ok: false, rejection: 'wrong-type' };
|
|
62
62
|
if (claims.aud !== expectedAudience)
|
|
63
63
|
return { ok: false, rejection: 'wrong-audience' };
|
|
@@ -73,6 +73,19 @@ const verifyAccessToken = (token, expectedAudience) => {
|
|
|
73
73
|
};
|
|
74
74
|
};
|
|
75
75
|
exports.verifyAccessToken = verifyAccessToken;
|
|
76
|
+
/**
|
|
77
|
+
* Tell an OAuth access token apart from an ordinary session JWT.
|
|
78
|
+
*
|
|
79
|
+
* They are signed with the same secret and arrive in the same header, so
|
|
80
|
+
* anything holding one has to ask which it got. Getting this wrong is not a
|
|
81
|
+
* subtle failure: read as a session, an access token has no `id` and no
|
|
82
|
+
* `groups`, so `req.user.id` lands as `undefined` and every scoped query goes
|
|
83
|
+
* out unbounded.
|
|
84
|
+
*/
|
|
85
|
+
const isAccessTokenClaims = (claims) => typeof claims === 'object' &&
|
|
86
|
+
claims !== null &&
|
|
87
|
+
claims.typ === exports.ACCESS_TOKEN_TYPE;
|
|
88
|
+
exports.isAccessTokenClaims = isAccessTokenClaims;
|
|
76
89
|
/** Parse a space-separated `scope` parameter, dropping anything we do not define. */
|
|
77
90
|
const parseScopes = (scope) => typeof scope === 'string' ? scope.split(/\s+/).filter(types_1.isApiKeyScope) : [];
|
|
78
91
|
exports.parseScopes = parseScopes;
|
package/dist/oauth/tokens.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/oauth/tokens.ts"],"names":[],"mappings":";;;;;;AAAA,mCAAoC;AACpC,gEAA+B;AAC/B,uCAA2C;AAC3C,4CAAmE;AAEnE;;;;;;;;;GASG;AAEH,oFAAoF;
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/oauth/tokens.ts"],"names":[],"mappings":"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"}
|