@drawbridge/drawbridge-utils 0.0.49 → 0.0.51
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/oauth/index.cjs +26 -3
- package/dist/oauth/index.d.cts +52 -3
- package/dist/oauth/index.d.ts +52 -3
- package/dist/oauth/index.js +25 -3
- package/dist/oauth/server.cjs +31 -30
- package/dist/oauth/server.d.cts +41 -32
- package/dist/oauth/server.d.ts +41 -32
- package/dist/oauth/server.js +31 -30
- package/package.json +1 -1
package/dist/oauth/index.cjs
CHANGED
|
@@ -47,6 +47,7 @@ __export(oauth_exports, {
|
|
|
47
47
|
scopeLabels: () => scopeLabels,
|
|
48
48
|
scopeOptions: () => scopeOptions,
|
|
49
49
|
scopes: () => scopes,
|
|
50
|
+
tokenFromHandshake: () => tokenFromHandshake,
|
|
50
51
|
tokenTypes: () => tokenTypes,
|
|
51
52
|
validate: () => validate
|
|
52
53
|
});
|
|
@@ -84,9 +85,30 @@ var clients = {
|
|
|
84
85
|
dashboard: "drawbridge-dashboard",
|
|
85
86
|
share: "drawbridge-share"
|
|
86
87
|
};
|
|
88
|
+
var tokenFromHandshake = (socket) => {
|
|
89
|
+
var _a, _b, _c;
|
|
90
|
+
if ((_b = (_a = socket == null ? void 0 : socket.handshake) == null ? void 0 : _a.auth) == null ? void 0 : _b.token) {
|
|
91
|
+
return socket.handshake.auth.token;
|
|
92
|
+
}
|
|
93
|
+
const headers = ((_c = socket == null ? void 0 : socket.handshake) == null ? void 0 : _c.headers) || {};
|
|
94
|
+
const subprotocols = (headers["sec-websocket-protocol"] || "").split(",").map((value) => value.trim());
|
|
95
|
+
for (const subprotocol of subprotocols) {
|
|
96
|
+
if (subprotocol.startsWith("bearer.")) {
|
|
97
|
+
return subprotocol.slice("bearer.".length) || null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!headers.cookie) return null;
|
|
101
|
+
const parsed = headers.cookie.split(";").reduce((accumulator, pair) => {
|
|
102
|
+
const index = pair.indexOf("=");
|
|
103
|
+
if (index < 0) return accumulator;
|
|
104
|
+
accumulator[pair.slice(0, index).trim()] = decodeURIComponent(pair.slice(index + 1).trim());
|
|
105
|
+
return accumulator;
|
|
106
|
+
}, {});
|
|
107
|
+
return parsed[cookies.session] || null;
|
|
108
|
+
};
|
|
87
109
|
var lifetimes = {
|
|
88
|
-
access: 60 * 60,
|
|
89
|
-
//
|
|
110
|
+
access: 60 * 60 * 8,
|
|
111
|
+
// 8 hours
|
|
90
112
|
authorizationCode: 10 * 60,
|
|
91
113
|
// 10 minutes
|
|
92
114
|
code: 15 * 60,
|
|
@@ -239,7 +261,7 @@ var createOAuthClient = ({
|
|
|
239
261
|
const body = await response.json();
|
|
240
262
|
cached = {
|
|
241
263
|
accessToken: body.access_token,
|
|
242
|
-
expiresAt: Math.floor(Date.now() / 1e3) + Number(body.expires_in ||
|
|
264
|
+
expiresAt: Math.floor(Date.now() / 1e3) + Number(body.expires_in || lifetimes.access),
|
|
243
265
|
scope: body.scope
|
|
244
266
|
};
|
|
245
267
|
return cached.accessToken;
|
|
@@ -283,6 +305,7 @@ var createOAuthClient = ({
|
|
|
283
305
|
scopeLabels,
|
|
284
306
|
scopeOptions,
|
|
285
307
|
scopes,
|
|
308
|
+
tokenFromHandshake,
|
|
286
309
|
tokenTypes,
|
|
287
310
|
validate
|
|
288
311
|
});
|
package/dist/oauth/index.d.cts
CHANGED
|
@@ -32,10 +32,59 @@ const clients = {
|
|
|
32
32
|
share : 'drawbridge-share'
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Resolve the raw OAuth access token from a socket.io handshake. Precedence:
|
|
36
|
+
// 1. handshake.auth.token — the browser client connects with
|
|
37
|
+
// io( uri, { auth : { token } } ); socket.io exposes it here.
|
|
38
|
+
// 2. Sec-WebSocket-Protocol: bearer.<token> subprotocol.
|
|
39
|
+
// 3. the drawbridge.session cookie on the upgrade request.
|
|
40
|
+
// This is the OAuth handshake token-source contract; drawbridge-sync's WS auth
|
|
41
|
+
// reads it so any future socket-authenticating service resolves it identically.
|
|
42
|
+
const tokenFromHandshake = ( socket ) => {
|
|
43
|
+
|
|
44
|
+
if( socket?.handshake?.auth?.token ){
|
|
45
|
+
|
|
46
|
+
return socket.handshake.auth.token;
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const headers = socket?.handshake?.headers || {};
|
|
51
|
+
|
|
52
|
+
const subprotocols = ( headers[ 'sec-websocket-protocol' ] || '' )
|
|
53
|
+
.split( ',' )
|
|
54
|
+
.map( ( value ) => value.trim() );
|
|
55
|
+
|
|
56
|
+
for( const subprotocol of subprotocols ){
|
|
57
|
+
|
|
58
|
+
if( subprotocol.startsWith( 'bearer.' ) ){
|
|
59
|
+
|
|
60
|
+
return subprotocol.slice( 'bearer.'.length ) || null;
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if( ! headers.cookie ) return null;
|
|
67
|
+
|
|
68
|
+
const parsed = headers.cookie.split( ';' ).reduce( ( accumulator, pair ) => {
|
|
69
|
+
|
|
70
|
+
const index = pair.indexOf( '=' );
|
|
71
|
+
|
|
72
|
+
if( index < 0 ) return accumulator;
|
|
73
|
+
|
|
74
|
+
accumulator[ pair.slice( 0, index ).trim() ] = decodeURIComponent( pair.slice( index + 1 ).trim() );
|
|
75
|
+
|
|
76
|
+
return accumulator;
|
|
77
|
+
|
|
78
|
+
}, {} );
|
|
79
|
+
|
|
80
|
+
return parsed[ cookies.session ] || null;
|
|
81
|
+
|
|
82
|
+
};
|
|
83
|
+
|
|
35
84
|
// Token / authorization-code / OTC lifetimes in seconds (the OAuth spec's
|
|
36
85
|
// unit for `expires_in`). Multiply by 1000 when feeding to JS Date math.
|
|
37
86
|
const lifetimes = {
|
|
38
|
-
access : 60 * 60,
|
|
87
|
+
access : 60 * 60 * 8, // 8 hours
|
|
39
88
|
authorizationCode : 10 * 60, // 10 minutes
|
|
40
89
|
code : 15 * 60, // 15 minutes (email OTC)
|
|
41
90
|
refresh : 30 * 24 * 60 * 60 // 30 days
|
|
@@ -304,7 +353,7 @@ const createOAuthClient = ({
|
|
|
304
353
|
|
|
305
354
|
cached = {
|
|
306
355
|
accessToken : body.access_token,
|
|
307
|
-
expiresAt : Math.floor( Date.now() / 1000 ) + Number( body.expires_in ||
|
|
356
|
+
expiresAt : Math.floor( Date.now() / 1000 ) + Number( body.expires_in || lifetimes.access ),
|
|
308
357
|
scope : body.scope
|
|
309
358
|
};
|
|
310
359
|
|
|
@@ -346,4 +395,4 @@ const createOAuthClient = ({
|
|
|
346
395
|
|
|
347
396
|
};
|
|
348
397
|
|
|
349
|
-
export { clients, cookieOptions, cookies, createOAuthClient, developer, developerOptions, generateToken, hashToken, intersect, isDeveloperAllowed, lifetimes, menuEndpoint, otcContexts, parse, scopeLabels, scopeOptions, scopes, tokenTypes, validate };
|
|
398
|
+
export { clients, cookieOptions, cookies, createOAuthClient, developer, developerOptions, generateToken, hashToken, intersect, isDeveloperAllowed, lifetimes, menuEndpoint, otcContexts, parse, scopeLabels, scopeOptions, scopes, tokenFromHandshake, tokenTypes, validate };
|
package/dist/oauth/index.d.ts
CHANGED
|
@@ -32,10 +32,59 @@ const clients = {
|
|
|
32
32
|
share : 'drawbridge-share'
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Resolve the raw OAuth access token from a socket.io handshake. Precedence:
|
|
36
|
+
// 1. handshake.auth.token — the browser client connects with
|
|
37
|
+
// io( uri, { auth : { token } } ); socket.io exposes it here.
|
|
38
|
+
// 2. Sec-WebSocket-Protocol: bearer.<token> subprotocol.
|
|
39
|
+
// 3. the drawbridge.session cookie on the upgrade request.
|
|
40
|
+
// This is the OAuth handshake token-source contract; drawbridge-sync's WS auth
|
|
41
|
+
// reads it so any future socket-authenticating service resolves it identically.
|
|
42
|
+
const tokenFromHandshake = ( socket ) => {
|
|
43
|
+
|
|
44
|
+
if( socket?.handshake?.auth?.token ){
|
|
45
|
+
|
|
46
|
+
return socket.handshake.auth.token;
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const headers = socket?.handshake?.headers || {};
|
|
51
|
+
|
|
52
|
+
const subprotocols = ( headers[ 'sec-websocket-protocol' ] || '' )
|
|
53
|
+
.split( ',' )
|
|
54
|
+
.map( ( value ) => value.trim() );
|
|
55
|
+
|
|
56
|
+
for( const subprotocol of subprotocols ){
|
|
57
|
+
|
|
58
|
+
if( subprotocol.startsWith( 'bearer.' ) ){
|
|
59
|
+
|
|
60
|
+
return subprotocol.slice( 'bearer.'.length ) || null;
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if( ! headers.cookie ) return null;
|
|
67
|
+
|
|
68
|
+
const parsed = headers.cookie.split( ';' ).reduce( ( accumulator, pair ) => {
|
|
69
|
+
|
|
70
|
+
const index = pair.indexOf( '=' );
|
|
71
|
+
|
|
72
|
+
if( index < 0 ) return accumulator;
|
|
73
|
+
|
|
74
|
+
accumulator[ pair.slice( 0, index ).trim() ] = decodeURIComponent( pair.slice( index + 1 ).trim() );
|
|
75
|
+
|
|
76
|
+
return accumulator;
|
|
77
|
+
|
|
78
|
+
}, {} );
|
|
79
|
+
|
|
80
|
+
return parsed[ cookies.session ] || null;
|
|
81
|
+
|
|
82
|
+
};
|
|
83
|
+
|
|
35
84
|
// Token / authorization-code / OTC lifetimes in seconds (the OAuth spec's
|
|
36
85
|
// unit for `expires_in`). Multiply by 1000 when feeding to JS Date math.
|
|
37
86
|
const lifetimes = {
|
|
38
|
-
access : 60 * 60,
|
|
87
|
+
access : 60 * 60 * 8, // 8 hours
|
|
39
88
|
authorizationCode : 10 * 60, // 10 minutes
|
|
40
89
|
code : 15 * 60, // 15 minutes (email OTC)
|
|
41
90
|
refresh : 30 * 24 * 60 * 60 // 30 days
|
|
@@ -304,7 +353,7 @@ const createOAuthClient = ({
|
|
|
304
353
|
|
|
305
354
|
cached = {
|
|
306
355
|
accessToken : body.access_token,
|
|
307
|
-
expiresAt : Math.floor( Date.now() / 1000 ) + Number( body.expires_in ||
|
|
356
|
+
expiresAt : Math.floor( Date.now() / 1000 ) + Number( body.expires_in || lifetimes.access ),
|
|
308
357
|
scope : body.scope
|
|
309
358
|
};
|
|
310
359
|
|
|
@@ -346,4 +395,4 @@ const createOAuthClient = ({
|
|
|
346
395
|
|
|
347
396
|
};
|
|
348
397
|
|
|
349
|
-
export { clients, cookieOptions, cookies, createOAuthClient, developer, developerOptions, generateToken, hashToken, intersect, isDeveloperAllowed, lifetimes, menuEndpoint, otcContexts, parse, scopeLabels, scopeOptions, scopes, tokenTypes, validate };
|
|
398
|
+
export { clients, cookieOptions, cookies, createOAuthClient, developer, developerOptions, generateToken, hashToken, intersect, isDeveloperAllowed, lifetimes, menuEndpoint, otcContexts, parse, scopeLabels, scopeOptions, scopes, tokenFromHandshake, tokenTypes, validate };
|
package/dist/oauth/index.js
CHANGED
|
@@ -30,9 +30,30 @@ var clients = {
|
|
|
30
30
|
dashboard: "drawbridge-dashboard",
|
|
31
31
|
share: "drawbridge-share"
|
|
32
32
|
};
|
|
33
|
+
var tokenFromHandshake = (socket) => {
|
|
34
|
+
var _a, _b, _c;
|
|
35
|
+
if ((_b = (_a = socket == null ? void 0 : socket.handshake) == null ? void 0 : _a.auth) == null ? void 0 : _b.token) {
|
|
36
|
+
return socket.handshake.auth.token;
|
|
37
|
+
}
|
|
38
|
+
const headers = ((_c = socket == null ? void 0 : socket.handshake) == null ? void 0 : _c.headers) || {};
|
|
39
|
+
const subprotocols = (headers["sec-websocket-protocol"] || "").split(",").map((value) => value.trim());
|
|
40
|
+
for (const subprotocol of subprotocols) {
|
|
41
|
+
if (subprotocol.startsWith("bearer.")) {
|
|
42
|
+
return subprotocol.slice("bearer.".length) || null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!headers.cookie) return null;
|
|
46
|
+
const parsed = headers.cookie.split(";").reduce((accumulator, pair) => {
|
|
47
|
+
const index = pair.indexOf("=");
|
|
48
|
+
if (index < 0) return accumulator;
|
|
49
|
+
accumulator[pair.slice(0, index).trim()] = decodeURIComponent(pair.slice(index + 1).trim());
|
|
50
|
+
return accumulator;
|
|
51
|
+
}, {});
|
|
52
|
+
return parsed[cookies.session] || null;
|
|
53
|
+
};
|
|
33
54
|
var lifetimes = {
|
|
34
|
-
access: 60 * 60,
|
|
35
|
-
//
|
|
55
|
+
access: 60 * 60 * 8,
|
|
56
|
+
// 8 hours
|
|
36
57
|
authorizationCode: 10 * 60,
|
|
37
58
|
// 10 minutes
|
|
38
59
|
code: 15 * 60,
|
|
@@ -185,7 +206,7 @@ var createOAuthClient = ({
|
|
|
185
206
|
const body = await response.json();
|
|
186
207
|
cached = {
|
|
187
208
|
accessToken: body.access_token,
|
|
188
|
-
expiresAt: Math.floor(Date.now() / 1e3) + Number(body.expires_in ||
|
|
209
|
+
expiresAt: Math.floor(Date.now() / 1e3) + Number(body.expires_in || lifetimes.access),
|
|
189
210
|
scope: body.scope
|
|
190
211
|
};
|
|
191
212
|
return cached.accessToken;
|
|
@@ -228,6 +249,7 @@ export {
|
|
|
228
249
|
scopeLabels,
|
|
229
250
|
scopeOptions,
|
|
230
251
|
scopes,
|
|
252
|
+
tokenFromHandshake,
|
|
231
253
|
tokenTypes,
|
|
232
254
|
validate
|
|
233
255
|
};
|
package/dist/oauth/server.cjs
CHANGED
|
@@ -60,8 +60,8 @@ var compare = (a, b) => {
|
|
|
60
60
|
|
|
61
61
|
// lib/oauth/index.js
|
|
62
62
|
var lifetimes = {
|
|
63
|
-
access: 60 * 60,
|
|
64
|
-
//
|
|
63
|
+
access: 60 * 60 * 8,
|
|
64
|
+
// 8 hours
|
|
65
65
|
authorizationCode: 10 * 60,
|
|
66
66
|
// 10 minutes
|
|
67
67
|
code: 15 * 60,
|
|
@@ -146,19 +146,20 @@ var hashToken = (raw) => hash(raw, process.env.OAUTH_TOKEN_HMAC_KEY);
|
|
|
146
146
|
var generateToken = () => generate(32, "base64url");
|
|
147
147
|
|
|
148
148
|
// lib/oauth/server.js
|
|
149
|
+
var SUPPORTED_GRANTS = ["authorization_code", "client_credentials", "refresh_token"];
|
|
149
150
|
var createOAuthModel = ({ controller }) => ({
|
|
150
151
|
getClient: async (clientId, clientSecret) => {
|
|
151
152
|
var _a;
|
|
152
|
-
const
|
|
153
|
+
const record = await controller.get({
|
|
153
154
|
collection: "oauth",
|
|
154
155
|
query: {
|
|
155
|
-
clientId,
|
|
156
|
+
client: clientId,
|
|
156
157
|
status: "active"
|
|
157
158
|
}
|
|
158
159
|
});
|
|
159
|
-
if (!
|
|
160
|
+
if (!record) return false;
|
|
160
161
|
if (clientSecret) {
|
|
161
|
-
const validSecret = (_a =
|
|
162
|
+
const validSecret = (_a = record.hashes) == null ? void 0 : _a.some((entry) => {
|
|
162
163
|
if (entry.retiredAt) return false;
|
|
163
164
|
return compare(hashToken(clientSecret), entry.hash);
|
|
164
165
|
});
|
|
@@ -166,11 +167,11 @@ var createOAuthModel = ({ controller }) => ({
|
|
|
166
167
|
}
|
|
167
168
|
;
|
|
168
169
|
return {
|
|
169
|
-
clientId: client
|
|
170
|
-
grants:
|
|
171
|
-
id:
|
|
172
|
-
redirectUris:
|
|
173
|
-
scopes:
|
|
170
|
+
clientId: record.client,
|
|
171
|
+
grants: SUPPORTED_GRANTS,
|
|
172
|
+
id: record.id,
|
|
173
|
+
redirectUris: record.redirects ?? [],
|
|
174
|
+
scopes: record.scopes ?? []
|
|
174
175
|
};
|
|
175
176
|
},
|
|
176
177
|
saveAuthorizationCode: async (code, client, user) => {
|
|
@@ -375,17 +376,16 @@ var createOAuthServer = ({ controller }) => new import_oauth2_server.default({
|
|
|
375
376
|
}
|
|
376
377
|
});
|
|
377
378
|
var createClient = async ({
|
|
378
|
-
allowedScopes = [],
|
|
379
379
|
controller,
|
|
380
380
|
createdBy = null,
|
|
381
381
|
description = null,
|
|
382
|
-
grantTypes = ["authorization_code"],
|
|
383
382
|
name,
|
|
384
383
|
organization = null,
|
|
385
|
-
|
|
386
|
-
|
|
384
|
+
redirects = [],
|
|
385
|
+
scopes: scopes2 = [],
|
|
386
|
+
origins = []
|
|
387
387
|
}) => {
|
|
388
|
-
const invalid =
|
|
388
|
+
const invalid = scopes2.filter((scope) => !developer.includes(scope));
|
|
389
389
|
if (invalid.length > 0) {
|
|
390
390
|
const error = new Error("Scopes not permitted for developer apps: " + invalid.join(", "));
|
|
391
391
|
error.status = 400;
|
|
@@ -393,37 +393,38 @@ var createClient = async ({
|
|
|
393
393
|
}
|
|
394
394
|
;
|
|
395
395
|
const rawClientId = generateToken();
|
|
396
|
-
const
|
|
396
|
+
const client = "drawbridge." + rawClientId;
|
|
397
397
|
const rawSecret = generateToken();
|
|
398
398
|
const secretHash = hashToken(rawSecret);
|
|
399
399
|
const now = /* @__PURE__ */ new Date();
|
|
400
|
-
const
|
|
400
|
+
const record = await controller.create({
|
|
401
401
|
collection: "oauth",
|
|
402
402
|
data: {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
403
|
+
client,
|
|
404
|
+
createdBy,
|
|
405
|
+
description,
|
|
406
|
+
endpoint: {
|
|
407
|
+
auth: "client_secret_basic"
|
|
408
|
+
},
|
|
409
|
+
hashes: [
|
|
406
410
|
{
|
|
407
411
|
createdAt: now,
|
|
408
412
|
hash: secretHash,
|
|
409
413
|
retiredAt: null
|
|
410
414
|
}
|
|
411
415
|
],
|
|
412
|
-
|
|
413
|
-
createdBy,
|
|
414
|
-
description,
|
|
415
|
-
grantTypes,
|
|
416
|
+
limits: null,
|
|
416
417
|
name,
|
|
417
418
|
organization,
|
|
418
|
-
|
|
419
|
-
|
|
419
|
+
origins,
|
|
420
|
+
redirects,
|
|
421
|
+
scopes: scopes2,
|
|
420
422
|
status: "active",
|
|
421
|
-
|
|
422
|
-
webOrigins
|
|
423
|
+
type: "confidential"
|
|
423
424
|
}
|
|
424
425
|
});
|
|
425
426
|
return {
|
|
426
|
-
|
|
427
|
+
record,
|
|
427
428
|
rawSecret
|
|
428
429
|
};
|
|
429
430
|
};
|
package/dist/oauth/server.d.cts
CHANGED
|
@@ -14,34 +14,43 @@ import 'crypto';
|
|
|
14
14
|
// { collection, query, data } works. drawbridge-api passes its base
|
|
15
15
|
// controller; tests can pass an in-memory mock.
|
|
16
16
|
//
|
|
17
|
+
// Boundary translation: the DB uses our renamed schema (client,
|
|
18
|
+
// scopes, redirects, hashes, ...), but the @node-oauth library
|
|
19
|
+
// expects spec-named keys (clientId, scope, redirectUris, ...).
|
|
20
|
+
// Each model method translates at the return boundary.
|
|
21
|
+
//
|
|
17
22
|
// - createOAuthServer({ controller })
|
|
18
23
|
// Convenience: creates the model + wraps it in an OAuth2Server with
|
|
19
24
|
// the standard Drawbridge lifetimes (1h access / 30d refresh) and
|
|
20
25
|
// client-authentication requirements.
|
|
21
26
|
//
|
|
22
|
-
// - createClient({
|
|
27
|
+
// - createClient({ controller, scopes, name, organization, ... })
|
|
23
28
|
// Inserts a new oauth client row. Validates scopes against the
|
|
24
29
|
// developer set; throws 400 on invalid. Returns
|
|
25
|
-
// {
|
|
30
|
+
// { record, rawSecret } — secret is plaintext, return ONCE.
|
|
31
|
+
|
|
26
32
|
|
|
33
|
+
// Hardcoded — the supported grants don't vary per client today. If they
|
|
34
|
+
// ever need to, store `grants` on the oauth doc and read it here.
|
|
35
|
+
const SUPPORTED_GRANTS = [ 'authorization_code', 'client_credentials', 'refresh_token' ];
|
|
27
36
|
|
|
28
37
|
const createOAuthModel = ({ controller }) => ({
|
|
29
38
|
|
|
30
39
|
getClient : async ( clientId, clientSecret ) => {
|
|
31
40
|
|
|
32
|
-
const
|
|
41
|
+
const record = await controller.get({
|
|
33
42
|
collection : 'oauth',
|
|
34
43
|
query : {
|
|
35
|
-
clientId,
|
|
44
|
+
client : clientId,
|
|
36
45
|
status : 'active'
|
|
37
46
|
}
|
|
38
47
|
});
|
|
39
48
|
|
|
40
|
-
if( !
|
|
49
|
+
if( ! record ) return false;
|
|
41
50
|
|
|
42
51
|
if( clientSecret ){
|
|
43
52
|
|
|
44
|
-
const validSecret =
|
|
53
|
+
const validSecret = record.hashes?.some( ( entry ) => {
|
|
45
54
|
|
|
46
55
|
if( entry.retiredAt ) return false;
|
|
47
56
|
|
|
@@ -53,11 +62,11 @@ const createOAuthModel = ({ controller }) => ({
|
|
|
53
62
|
|
|
54
63
|
}
|
|
55
64
|
return {
|
|
56
|
-
clientId : client
|
|
57
|
-
grants :
|
|
58
|
-
id :
|
|
59
|
-
redirectUris :
|
|
60
|
-
scopes :
|
|
65
|
+
clientId : record.client,
|
|
66
|
+
grants : SUPPORTED_GRANTS,
|
|
67
|
+
id : record.id,
|
|
68
|
+
redirectUris : record.redirects ?? [],
|
|
69
|
+
scopes : record.scopes ?? []
|
|
61
70
|
};
|
|
62
71
|
|
|
63
72
|
},
|
|
@@ -312,23 +321,22 @@ const createOAuthServer = ({ controller }) => new OAuth2Server({
|
|
|
312
321
|
}
|
|
313
322
|
});
|
|
314
323
|
|
|
315
|
-
// Inserts a new oauth client row. Validates
|
|
324
|
+
// Inserts a new oauth client row. Validates scopes against the
|
|
316
325
|
// developer-allowed subset; throws a 400-flagged error on invalid scopes.
|
|
317
|
-
// Returns {
|
|
326
|
+
// Returns { record: <inserted-doc>, rawSecret: <plaintext> }. The raw
|
|
318
327
|
// secret is only available here — store immediately.
|
|
319
328
|
const createClient = async ({
|
|
320
|
-
allowedScopes = [],
|
|
321
329
|
controller,
|
|
322
330
|
createdBy = null,
|
|
323
331
|
description = null,
|
|
324
|
-
grantTypes = [ 'authorization_code' ],
|
|
325
332
|
name,
|
|
326
333
|
organization = null,
|
|
327
|
-
|
|
328
|
-
|
|
334
|
+
redirects = [],
|
|
335
|
+
scopes = [],
|
|
336
|
+
origins = []
|
|
329
337
|
}) => {
|
|
330
338
|
|
|
331
|
-
const invalid =
|
|
339
|
+
const invalid = scopes.filter( ( scope ) => ! developer.includes( scope ) );
|
|
332
340
|
|
|
333
341
|
if( invalid.length > 0 ){
|
|
334
342
|
|
|
@@ -340,41 +348,42 @@ const createClient = async ({
|
|
|
340
348
|
|
|
341
349
|
}
|
|
342
350
|
const rawClientId = generateToken();
|
|
343
|
-
const
|
|
351
|
+
const client = 'drawbridge.' + rawClientId;
|
|
344
352
|
|
|
345
353
|
const rawSecret = generateToken();
|
|
346
354
|
const secretHash = hashToken( rawSecret );
|
|
347
355
|
|
|
348
356
|
const now = new Date();
|
|
349
357
|
|
|
350
|
-
const
|
|
358
|
+
const record = await controller.create({
|
|
351
359
|
collection : 'oauth',
|
|
352
360
|
data : {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
client,
|
|
362
|
+
createdBy,
|
|
363
|
+
description,
|
|
364
|
+
endpoint : {
|
|
365
|
+
auth : 'client_secret_basic'
|
|
366
|
+
},
|
|
367
|
+
hashes : [
|
|
356
368
|
{
|
|
357
369
|
createdAt : now,
|
|
358
370
|
hash : secretHash,
|
|
359
371
|
retiredAt : null
|
|
360
372
|
}
|
|
361
373
|
],
|
|
362
|
-
|
|
363
|
-
createdBy,
|
|
364
|
-
description,
|
|
365
|
-
grantTypes,
|
|
374
|
+
limits : null,
|
|
366
375
|
name,
|
|
367
376
|
organization,
|
|
368
|
-
|
|
369
|
-
|
|
377
|
+
origins,
|
|
378
|
+
redirects,
|
|
379
|
+
scopes,
|
|
370
380
|
status : 'active',
|
|
371
|
-
|
|
372
|
-
webOrigins
|
|
381
|
+
type : 'confidential'
|
|
373
382
|
}
|
|
374
383
|
});
|
|
375
384
|
|
|
376
385
|
return {
|
|
377
|
-
|
|
386
|
+
record,
|
|
378
387
|
rawSecret
|
|
379
388
|
};
|
|
380
389
|
|
package/dist/oauth/server.d.ts
CHANGED
|
@@ -14,34 +14,43 @@ import 'crypto';
|
|
|
14
14
|
// { collection, query, data } works. drawbridge-api passes its base
|
|
15
15
|
// controller; tests can pass an in-memory mock.
|
|
16
16
|
//
|
|
17
|
+
// Boundary translation: the DB uses our renamed schema (client,
|
|
18
|
+
// scopes, redirects, hashes, ...), but the @node-oauth library
|
|
19
|
+
// expects spec-named keys (clientId, scope, redirectUris, ...).
|
|
20
|
+
// Each model method translates at the return boundary.
|
|
21
|
+
//
|
|
17
22
|
// - createOAuthServer({ controller })
|
|
18
23
|
// Convenience: creates the model + wraps it in an OAuth2Server with
|
|
19
24
|
// the standard Drawbridge lifetimes (1h access / 30d refresh) and
|
|
20
25
|
// client-authentication requirements.
|
|
21
26
|
//
|
|
22
|
-
// - createClient({
|
|
27
|
+
// - createClient({ controller, scopes, name, organization, ... })
|
|
23
28
|
// Inserts a new oauth client row. Validates scopes against the
|
|
24
29
|
// developer set; throws 400 on invalid. Returns
|
|
25
|
-
// {
|
|
30
|
+
// { record, rawSecret } — secret is plaintext, return ONCE.
|
|
31
|
+
|
|
26
32
|
|
|
33
|
+
// Hardcoded — the supported grants don't vary per client today. If they
|
|
34
|
+
// ever need to, store `grants` on the oauth doc and read it here.
|
|
35
|
+
const SUPPORTED_GRANTS = [ 'authorization_code', 'client_credentials', 'refresh_token' ];
|
|
27
36
|
|
|
28
37
|
const createOAuthModel = ({ controller }) => ({
|
|
29
38
|
|
|
30
39
|
getClient : async ( clientId, clientSecret ) => {
|
|
31
40
|
|
|
32
|
-
const
|
|
41
|
+
const record = await controller.get({
|
|
33
42
|
collection : 'oauth',
|
|
34
43
|
query : {
|
|
35
|
-
clientId,
|
|
44
|
+
client : clientId,
|
|
36
45
|
status : 'active'
|
|
37
46
|
}
|
|
38
47
|
});
|
|
39
48
|
|
|
40
|
-
if( !
|
|
49
|
+
if( ! record ) return false;
|
|
41
50
|
|
|
42
51
|
if( clientSecret ){
|
|
43
52
|
|
|
44
|
-
const validSecret =
|
|
53
|
+
const validSecret = record.hashes?.some( ( entry ) => {
|
|
45
54
|
|
|
46
55
|
if( entry.retiredAt ) return false;
|
|
47
56
|
|
|
@@ -53,11 +62,11 @@ const createOAuthModel = ({ controller }) => ({
|
|
|
53
62
|
|
|
54
63
|
}
|
|
55
64
|
return {
|
|
56
|
-
clientId : client
|
|
57
|
-
grants :
|
|
58
|
-
id :
|
|
59
|
-
redirectUris :
|
|
60
|
-
scopes :
|
|
65
|
+
clientId : record.client,
|
|
66
|
+
grants : SUPPORTED_GRANTS,
|
|
67
|
+
id : record.id,
|
|
68
|
+
redirectUris : record.redirects ?? [],
|
|
69
|
+
scopes : record.scopes ?? []
|
|
61
70
|
};
|
|
62
71
|
|
|
63
72
|
},
|
|
@@ -312,23 +321,22 @@ const createOAuthServer = ({ controller }) => new OAuth2Server({
|
|
|
312
321
|
}
|
|
313
322
|
});
|
|
314
323
|
|
|
315
|
-
// Inserts a new oauth client row. Validates
|
|
324
|
+
// Inserts a new oauth client row. Validates scopes against the
|
|
316
325
|
// developer-allowed subset; throws a 400-flagged error on invalid scopes.
|
|
317
|
-
// Returns {
|
|
326
|
+
// Returns { record: <inserted-doc>, rawSecret: <plaintext> }. The raw
|
|
318
327
|
// secret is only available here — store immediately.
|
|
319
328
|
const createClient = async ({
|
|
320
|
-
allowedScopes = [],
|
|
321
329
|
controller,
|
|
322
330
|
createdBy = null,
|
|
323
331
|
description = null,
|
|
324
|
-
grantTypes = [ 'authorization_code' ],
|
|
325
332
|
name,
|
|
326
333
|
organization = null,
|
|
327
|
-
|
|
328
|
-
|
|
334
|
+
redirects = [],
|
|
335
|
+
scopes = [],
|
|
336
|
+
origins = []
|
|
329
337
|
}) => {
|
|
330
338
|
|
|
331
|
-
const invalid =
|
|
339
|
+
const invalid = scopes.filter( ( scope ) => ! developer.includes( scope ) );
|
|
332
340
|
|
|
333
341
|
if( invalid.length > 0 ){
|
|
334
342
|
|
|
@@ -340,41 +348,42 @@ const createClient = async ({
|
|
|
340
348
|
|
|
341
349
|
}
|
|
342
350
|
const rawClientId = generateToken();
|
|
343
|
-
const
|
|
351
|
+
const client = 'drawbridge.' + rawClientId;
|
|
344
352
|
|
|
345
353
|
const rawSecret = generateToken();
|
|
346
354
|
const secretHash = hashToken( rawSecret );
|
|
347
355
|
|
|
348
356
|
const now = new Date();
|
|
349
357
|
|
|
350
|
-
const
|
|
358
|
+
const record = await controller.create({
|
|
351
359
|
collection : 'oauth',
|
|
352
360
|
data : {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
client,
|
|
362
|
+
createdBy,
|
|
363
|
+
description,
|
|
364
|
+
endpoint : {
|
|
365
|
+
auth : 'client_secret_basic'
|
|
366
|
+
},
|
|
367
|
+
hashes : [
|
|
356
368
|
{
|
|
357
369
|
createdAt : now,
|
|
358
370
|
hash : secretHash,
|
|
359
371
|
retiredAt : null
|
|
360
372
|
}
|
|
361
373
|
],
|
|
362
|
-
|
|
363
|
-
createdBy,
|
|
364
|
-
description,
|
|
365
|
-
grantTypes,
|
|
374
|
+
limits : null,
|
|
366
375
|
name,
|
|
367
376
|
organization,
|
|
368
|
-
|
|
369
|
-
|
|
377
|
+
origins,
|
|
378
|
+
redirects,
|
|
379
|
+
scopes,
|
|
370
380
|
status : 'active',
|
|
371
|
-
|
|
372
|
-
webOrigins
|
|
381
|
+
type : 'confidential'
|
|
373
382
|
}
|
|
374
383
|
});
|
|
375
384
|
|
|
376
385
|
return {
|
|
377
|
-
|
|
386
|
+
record,
|
|
378
387
|
rawSecret
|
|
379
388
|
};
|
|
380
389
|
|
package/dist/oauth/server.js
CHANGED
|
@@ -25,8 +25,8 @@ var compare = (a, b) => {
|
|
|
25
25
|
|
|
26
26
|
// lib/oauth/index.js
|
|
27
27
|
var lifetimes = {
|
|
28
|
-
access: 60 * 60,
|
|
29
|
-
//
|
|
28
|
+
access: 60 * 60 * 8,
|
|
29
|
+
// 8 hours
|
|
30
30
|
authorizationCode: 10 * 60,
|
|
31
31
|
// 10 minutes
|
|
32
32
|
code: 15 * 60,
|
|
@@ -111,19 +111,20 @@ var hashToken = (raw) => hash(raw, process.env.OAUTH_TOKEN_HMAC_KEY);
|
|
|
111
111
|
var generateToken = () => generate(32, "base64url");
|
|
112
112
|
|
|
113
113
|
// lib/oauth/server.js
|
|
114
|
+
var SUPPORTED_GRANTS = ["authorization_code", "client_credentials", "refresh_token"];
|
|
114
115
|
var createOAuthModel = ({ controller }) => ({
|
|
115
116
|
getClient: async (clientId, clientSecret) => {
|
|
116
117
|
var _a;
|
|
117
|
-
const
|
|
118
|
+
const record = await controller.get({
|
|
118
119
|
collection: "oauth",
|
|
119
120
|
query: {
|
|
120
|
-
clientId,
|
|
121
|
+
client: clientId,
|
|
121
122
|
status: "active"
|
|
122
123
|
}
|
|
123
124
|
});
|
|
124
|
-
if (!
|
|
125
|
+
if (!record) return false;
|
|
125
126
|
if (clientSecret) {
|
|
126
|
-
const validSecret = (_a =
|
|
127
|
+
const validSecret = (_a = record.hashes) == null ? void 0 : _a.some((entry) => {
|
|
127
128
|
if (entry.retiredAt) return false;
|
|
128
129
|
return compare(hashToken(clientSecret), entry.hash);
|
|
129
130
|
});
|
|
@@ -131,11 +132,11 @@ var createOAuthModel = ({ controller }) => ({
|
|
|
131
132
|
}
|
|
132
133
|
;
|
|
133
134
|
return {
|
|
134
|
-
clientId: client
|
|
135
|
-
grants:
|
|
136
|
-
id:
|
|
137
|
-
redirectUris:
|
|
138
|
-
scopes:
|
|
135
|
+
clientId: record.client,
|
|
136
|
+
grants: SUPPORTED_GRANTS,
|
|
137
|
+
id: record.id,
|
|
138
|
+
redirectUris: record.redirects ?? [],
|
|
139
|
+
scopes: record.scopes ?? []
|
|
139
140
|
};
|
|
140
141
|
},
|
|
141
142
|
saveAuthorizationCode: async (code, client, user) => {
|
|
@@ -340,17 +341,16 @@ var createOAuthServer = ({ controller }) => new OAuth2Server({
|
|
|
340
341
|
}
|
|
341
342
|
});
|
|
342
343
|
var createClient = async ({
|
|
343
|
-
allowedScopes = [],
|
|
344
344
|
controller,
|
|
345
345
|
createdBy = null,
|
|
346
346
|
description = null,
|
|
347
|
-
grantTypes = ["authorization_code"],
|
|
348
347
|
name,
|
|
349
348
|
organization = null,
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
redirects = [],
|
|
350
|
+
scopes: scopes2 = [],
|
|
351
|
+
origins = []
|
|
352
352
|
}) => {
|
|
353
|
-
const invalid =
|
|
353
|
+
const invalid = scopes2.filter((scope) => !developer.includes(scope));
|
|
354
354
|
if (invalid.length > 0) {
|
|
355
355
|
const error = new Error("Scopes not permitted for developer apps: " + invalid.join(", "));
|
|
356
356
|
error.status = 400;
|
|
@@ -358,37 +358,38 @@ var createClient = async ({
|
|
|
358
358
|
}
|
|
359
359
|
;
|
|
360
360
|
const rawClientId = generateToken();
|
|
361
|
-
const
|
|
361
|
+
const client = "drawbridge." + rawClientId;
|
|
362
362
|
const rawSecret = generateToken();
|
|
363
363
|
const secretHash = hashToken(rawSecret);
|
|
364
364
|
const now = /* @__PURE__ */ new Date();
|
|
365
|
-
const
|
|
365
|
+
const record = await controller.create({
|
|
366
366
|
collection: "oauth",
|
|
367
367
|
data: {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
client,
|
|
369
|
+
createdBy,
|
|
370
|
+
description,
|
|
371
|
+
endpoint: {
|
|
372
|
+
auth: "client_secret_basic"
|
|
373
|
+
},
|
|
374
|
+
hashes: [
|
|
371
375
|
{
|
|
372
376
|
createdAt: now,
|
|
373
377
|
hash: secretHash,
|
|
374
378
|
retiredAt: null
|
|
375
379
|
}
|
|
376
380
|
],
|
|
377
|
-
|
|
378
|
-
createdBy,
|
|
379
|
-
description,
|
|
380
|
-
grantTypes,
|
|
381
|
+
limits: null,
|
|
381
382
|
name,
|
|
382
383
|
organization,
|
|
383
|
-
|
|
384
|
-
|
|
384
|
+
origins,
|
|
385
|
+
redirects,
|
|
386
|
+
scopes: scopes2,
|
|
385
387
|
status: "active",
|
|
386
|
-
|
|
387
|
-
webOrigins
|
|
388
|
+
type: "confidential"
|
|
388
389
|
}
|
|
389
390
|
});
|
|
390
391
|
return {
|
|
391
|
-
|
|
392
|
+
record,
|
|
392
393
|
rawSecret
|
|
393
394
|
};
|
|
394
395
|
};
|
package/package.json
CHANGED