@boxyhq/saml-jackson 1.0.2 → 1.0.5
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/controller/oauth.js +148 -67
- package/dist/controller/utils.d.ts +3 -0
- package/dist/controller/utils.js +36 -1
- package/dist/typings.d.ts +10 -1
- package/package.json +18 -17
package/dist/controller/oauth.js
CHANGED
@@ -50,7 +50,7 @@ const redirect = __importStar(require("./oauth/redirect"));
|
|
50
50
|
const utils_1 = require("./utils");
|
51
51
|
const deflateRawAsync = (0, util_1.promisify)(zlib_1.deflateRaw);
|
52
52
|
const validateResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0, void 0, function* () {
|
53
|
-
const profile = yield saml20_1.default.
|
53
|
+
const profile = yield saml20_1.default.validate(rawResponse, validateOpts);
|
54
54
|
if (profile && profile.claims) {
|
55
55
|
// we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
|
56
56
|
profile.claims = claims_1.default.map(profile.claims);
|
@@ -61,9 +61,9 @@ const validateResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0
|
|
61
61
|
}
|
62
62
|
return profile;
|
63
63
|
});
|
64
|
-
function
|
64
|
+
function getEncodedTenantProduct(param) {
|
65
65
|
try {
|
66
|
-
const sp = new URLSearchParams(
|
66
|
+
const sp = new URLSearchParams(param);
|
67
67
|
const tenant = sp.get('tenant');
|
68
68
|
const product = sp.get('product');
|
69
69
|
if (tenant && product) {
|
@@ -128,7 +128,7 @@ class OAuthController {
|
|
128
128
|
}
|
129
129
|
authorize(body) {
|
130
130
|
return __awaiter(this, void 0, void 0, function* () {
|
131
|
-
const { response_type = 'code', client_id, redirect_uri, state, tenant, product, code_challenge, code_challenge_method = '',
|
131
|
+
const { response_type = 'code', client_id, redirect_uri, state, tenant, product, access_type, scope, code_challenge, code_challenge_method = '',
|
132
132
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
133
133
|
provider = 'saml', idp_hint, } = body;
|
134
134
|
let requestedTenant = tenant;
|
@@ -137,9 +137,6 @@ class OAuthController {
|
|
137
137
|
if (!redirect_uri) {
|
138
138
|
throw new error_1.JacksonError('Please specify a redirect URL.', 400);
|
139
139
|
}
|
140
|
-
if (!state) {
|
141
|
-
throw new error_1.JacksonError('Please specify a state to safeguard against XSRF attacks.', 400);
|
142
|
-
}
|
143
140
|
let samlConfig;
|
144
141
|
if (tenant && product) {
|
145
142
|
const samlConfigs = yield this.configStore.getByIndex({
|
@@ -171,7 +168,13 @@ class OAuthController {
|
|
171
168
|
}
|
172
169
|
else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
|
173
170
|
// if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
|
174
|
-
|
171
|
+
let sp = getEncodedTenantProduct(client_id);
|
172
|
+
if (!sp && access_type) {
|
173
|
+
sp = getEncodedTenantProduct(access_type);
|
174
|
+
}
|
175
|
+
if (!sp && scope) {
|
176
|
+
sp = getEncodedTenantProduct(scope);
|
177
|
+
}
|
175
178
|
if (sp && sp.tenant && sp.product) {
|
176
179
|
requestedTenant = sp.tenant;
|
177
180
|
requestedProduct = sp.product;
|
@@ -204,6 +207,10 @@ class OAuthController {
|
|
204
207
|
}
|
205
208
|
else {
|
206
209
|
samlConfig = yield this.configStore.get(client_id);
|
210
|
+
if (samlConfig) {
|
211
|
+
requestedTenant = samlConfig.tenant;
|
212
|
+
requestedProduct = samlConfig.product;
|
213
|
+
}
|
207
214
|
}
|
208
215
|
}
|
209
216
|
else {
|
@@ -215,6 +222,25 @@ class OAuthController {
|
|
215
222
|
if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
|
216
223
|
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
217
224
|
}
|
225
|
+
if (!state) {
|
226
|
+
return {
|
227
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
228
|
+
error: 'invalid_request',
|
229
|
+
error_description: 'Please specify a state to safeguard against XSRF attacks',
|
230
|
+
redirect_uri,
|
231
|
+
}),
|
232
|
+
};
|
233
|
+
}
|
234
|
+
if (response_type !== 'code') {
|
235
|
+
return {
|
236
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
237
|
+
error: 'unsupported_response_type',
|
238
|
+
error_description: 'Only Authorization Code grant is supported',
|
239
|
+
redirect_uri,
|
240
|
+
state,
|
241
|
+
}),
|
242
|
+
};
|
243
|
+
}
|
218
244
|
let ssoUrl;
|
219
245
|
let post = false;
|
220
246
|
const { sso } = samlConfig.idpMetadata;
|
@@ -227,63 +253,86 @@ class OAuthController {
|
|
227
253
|
ssoUrl = sso.postUrl;
|
228
254
|
post = true;
|
229
255
|
}
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
if (requestedTenant) {
|
240
|
-
requested.tenant = requestedTenant;
|
241
|
-
}
|
242
|
-
if (requestedProduct) {
|
243
|
-
requested.product = requestedProduct;
|
256
|
+
else {
|
257
|
+
return {
|
258
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
259
|
+
error: 'invalid_request',
|
260
|
+
error_description: 'SAML binding could not be retrieved',
|
261
|
+
redirect_uri,
|
262
|
+
state,
|
263
|
+
}),
|
264
|
+
};
|
244
265
|
}
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
state,
|
253
|
-
code_challenge,
|
254
|
-
code_challenge_method,
|
255
|
-
requested,
|
256
|
-
});
|
257
|
-
const relayState = utils_1.relayStatePrefix + sessionId;
|
258
|
-
let redirectUrl;
|
259
|
-
let authorizeForm;
|
260
|
-
if (!post) {
|
261
|
-
// HTTP Redirect binding
|
262
|
-
redirectUrl = redirect.success(ssoUrl, {
|
263
|
-
RelayState: relayState,
|
264
|
-
SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
|
266
|
+
try {
|
267
|
+
const samlReq = saml20_1.default.request({
|
268
|
+
ssoUrl,
|
269
|
+
entityID: this.opts.samlAudience,
|
270
|
+
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
|
271
|
+
signingKey: samlConfig.certs.privateKey,
|
272
|
+
publicKey: samlConfig.certs.publicKey,
|
265
273
|
});
|
274
|
+
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
275
|
+
const requested = { client_id, state };
|
276
|
+
if (requestedTenant) {
|
277
|
+
requested.tenant = requestedTenant;
|
278
|
+
}
|
279
|
+
if (requestedProduct) {
|
280
|
+
requested.product = requestedProduct;
|
281
|
+
}
|
282
|
+
if (idp_hint) {
|
283
|
+
requested.idp_hint = idp_hint;
|
284
|
+
}
|
285
|
+
yield this.sessionStore.put(sessionId, {
|
286
|
+
id: samlReq.id,
|
287
|
+
redirect_uri,
|
288
|
+
response_type,
|
289
|
+
state,
|
290
|
+
code_challenge,
|
291
|
+
code_challenge_method,
|
292
|
+
requested,
|
293
|
+
});
|
294
|
+
const relayState = utils_1.relayStatePrefix + sessionId;
|
295
|
+
let redirectUrl;
|
296
|
+
let authorizeForm;
|
297
|
+
if (!post) {
|
298
|
+
// HTTP Redirect binding
|
299
|
+
redirectUrl = redirect.success(ssoUrl, {
|
300
|
+
RelayState: relayState,
|
301
|
+
SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
|
302
|
+
});
|
303
|
+
}
|
304
|
+
else {
|
305
|
+
// HTTP POST binding
|
306
|
+
authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
|
307
|
+
{
|
308
|
+
name: 'RelayState',
|
309
|
+
value: relayState,
|
310
|
+
},
|
311
|
+
{
|
312
|
+
name: 'SAMLRequest',
|
313
|
+
value: Buffer.from(samlReq.request).toString('base64'),
|
314
|
+
},
|
315
|
+
]);
|
316
|
+
}
|
317
|
+
return {
|
318
|
+
redirect_url: redirectUrl,
|
319
|
+
authorize_form: authorizeForm,
|
320
|
+
};
|
321
|
+
}
|
322
|
+
catch (err) {
|
323
|
+
return {
|
324
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
325
|
+
error: 'server_error',
|
326
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
327
|
+
redirect_uri,
|
328
|
+
state,
|
329
|
+
}),
|
330
|
+
};
|
266
331
|
}
|
267
|
-
else {
|
268
|
-
// HTTP POST binding
|
269
|
-
authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
|
270
|
-
{
|
271
|
-
name: 'RelayState',
|
272
|
-
value: relayState,
|
273
|
-
},
|
274
|
-
{
|
275
|
-
name: 'SAMLRequest',
|
276
|
-
value: Buffer.from(samlReq.request).toString('base64'),
|
277
|
-
},
|
278
|
-
]);
|
279
|
-
}
|
280
|
-
return {
|
281
|
-
redirect_url: redirectUrl,
|
282
|
-
authorize_form: authorizeForm,
|
283
|
-
};
|
284
332
|
});
|
285
333
|
}
|
286
334
|
samlResponse(body) {
|
335
|
+
var _a, _b;
|
287
336
|
return __awaiter(this, void 0, void 0, function* () {
|
288
337
|
const { SAMLResponse, idp_hint } = body;
|
289
338
|
let RelayState = body.RelayState || ''; // RelayState will contain the sessionId from earlier quasi-oauth flow
|
@@ -294,10 +343,13 @@ class OAuthController {
|
|
294
343
|
}
|
295
344
|
RelayState = RelayState.replace(utils_1.relayStatePrefix, '');
|
296
345
|
const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
|
297
|
-
const
|
346
|
+
const issuer = saml20_1.default.parseIssuer(rawResponse);
|
347
|
+
if (!issuer) {
|
348
|
+
throw new error_1.JacksonError('Issuer not found.', 403);
|
349
|
+
}
|
298
350
|
const samlConfigs = yield this.configStore.getByIndex({
|
299
351
|
name: utils_1.IndexNames.EntityID,
|
300
|
-
value:
|
352
|
+
value: issuer,
|
301
353
|
});
|
302
354
|
if (!samlConfigs || samlConfigs.length === 0) {
|
303
355
|
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
@@ -337,11 +389,30 @@ class OAuthController {
|
|
337
389
|
const validateOpts = {
|
338
390
|
thumbprint: samlConfig.idpMetadata.thumbprint,
|
339
391
|
audience: this.opts.samlAudience,
|
392
|
+
privateKey: samlConfig.certs.privateKey,
|
340
393
|
};
|
394
|
+
if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
|
395
|
+
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
396
|
+
}
|
341
397
|
if (session && session.id) {
|
342
398
|
validateOpts.inResponseTo = session.id;
|
343
399
|
}
|
344
|
-
|
400
|
+
let profile;
|
401
|
+
const redirect_uri = (session && session.redirect_uri) || samlConfig.defaultRedirectUrl;
|
402
|
+
try {
|
403
|
+
profile = yield validateResponse(rawResponse, validateOpts);
|
404
|
+
}
|
405
|
+
catch (err) {
|
406
|
+
// return error to redirect_uri
|
407
|
+
return {
|
408
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
409
|
+
error: 'access_denied',
|
410
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
411
|
+
redirect_uri,
|
412
|
+
state: (_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.state,
|
413
|
+
}),
|
414
|
+
};
|
415
|
+
}
|
345
416
|
// store details against a code
|
346
417
|
const code = crypto_1.default.randomBytes(20).toString('hex');
|
347
418
|
const codeVal = {
|
@@ -353,9 +424,19 @@ class OAuthController {
|
|
353
424
|
if (session) {
|
354
425
|
codeVal.session = session;
|
355
426
|
}
|
356
|
-
|
357
|
-
|
358
|
-
|
427
|
+
try {
|
428
|
+
yield this.codeStore.put(code, codeVal);
|
429
|
+
}
|
430
|
+
catch (err) {
|
431
|
+
// return error to redirect_uri
|
432
|
+
return {
|
433
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
434
|
+
error: 'server_error',
|
435
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
436
|
+
redirect_uri,
|
437
|
+
state: (_b = session === null || session === void 0 ? void 0 : session.requested) === null || _b === void 0 ? void 0 : _b.state,
|
438
|
+
}),
|
439
|
+
};
|
359
440
|
}
|
360
441
|
const params = {
|
361
442
|
code,
|
@@ -363,7 +444,7 @@ class OAuthController {
|
|
363
444
|
if (session && session.state) {
|
364
445
|
params.state = session.state;
|
365
446
|
}
|
366
|
-
const redirectUrl = redirect.success(
|
447
|
+
const redirectUrl = redirect.success(redirect_uri, params);
|
367
448
|
// delete the session
|
368
449
|
try {
|
369
450
|
yield this.sessionStore.delete(RelayState);
|
@@ -456,7 +537,7 @@ class OAuthController {
|
|
456
537
|
else if (client_id && client_secret) {
|
457
538
|
// check if we have an encoded client_id
|
458
539
|
if (client_id !== 'dummy') {
|
459
|
-
const sp =
|
540
|
+
const sp = getEncodedTenantProduct(client_id);
|
460
541
|
if (!sp) {
|
461
542
|
// OAuth flow
|
462
543
|
if (client_id !== codeVal.clientID || client_secret !== codeVal.clientSecret) {
|
@@ -1,6 +1,9 @@
|
|
1
|
+
import type { OAuthErrorHandlerParams } from '../typings';
|
1
2
|
export declare enum IndexNames {
|
2
3
|
EntityID = "entityID",
|
3
4
|
TenantProduct = "tenantProduct"
|
4
5
|
}
|
5
6
|
export declare const relayStatePrefix = "boxyhq_jackson_";
|
6
7
|
export declare const validateAbsoluteUrl: (url: any, message: any) => void;
|
8
|
+
export declare const OAuthErrorResponse: ({ error, error_description, redirect_uri, state, }: OAuthErrorHandlerParams) => string;
|
9
|
+
export declare function getErrorMessage(error: unknown): string;
|
package/dist/controller/utils.js
CHANGED
@@ -1,7 +1,31 @@
|
|
1
1
|
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
19
|
+
if (mod && mod.__esModule) return mod;
|
20
|
+
var result = {};
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
22
|
+
__setModuleDefault(result, mod);
|
23
|
+
return result;
|
24
|
+
};
|
2
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.validateAbsoluteUrl = exports.relayStatePrefix = exports.IndexNames = void 0;
|
26
|
+
exports.getErrorMessage = exports.OAuthErrorResponse = exports.validateAbsoluteUrl = exports.relayStatePrefix = exports.IndexNames = void 0;
|
4
27
|
const error_1 = require("./error");
|
28
|
+
const redirect = __importStar(require("./oauth/redirect"));
|
5
29
|
var IndexNames;
|
6
30
|
(function (IndexNames) {
|
7
31
|
IndexNames["EntityID"] = "entityID";
|
@@ -17,3 +41,14 @@ const validateAbsoluteUrl = (url, message) => {
|
|
17
41
|
}
|
18
42
|
};
|
19
43
|
exports.validateAbsoluteUrl = validateAbsoluteUrl;
|
44
|
+
const OAuthErrorResponse = ({ error, error_description, redirect_uri, state, }) => {
|
45
|
+
return redirect.success(redirect_uri, { error, error_description, state });
|
46
|
+
};
|
47
|
+
exports.OAuthErrorResponse = OAuthErrorResponse;
|
48
|
+
// https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript
|
49
|
+
function getErrorMessage(error) {
|
50
|
+
if (error instanceof Error)
|
51
|
+
return error.message;
|
52
|
+
return String(error);
|
53
|
+
}
|
54
|
+
exports.getErrorMessage = getErrorMessage;
|
package/dist/typings.d.ts
CHANGED
@@ -51,6 +51,8 @@ export interface OAuthReqBody {
|
|
51
51
|
state: string;
|
52
52
|
tenant?: string;
|
53
53
|
product?: string;
|
54
|
+
access_type?: string;
|
55
|
+
scope?: string;
|
54
56
|
code_challenge: string;
|
55
57
|
code_challenge_method: 'plain' | 'S256' | '';
|
56
58
|
provider: 'saml';
|
@@ -78,6 +80,7 @@ export interface Profile {
|
|
78
80
|
email: string;
|
79
81
|
firstName: string;
|
80
82
|
lastName: string;
|
83
|
+
requested: Record<string, string>;
|
81
84
|
}
|
82
85
|
export interface Index {
|
83
86
|
name: string;
|
@@ -141,7 +144,7 @@ interface Metadata {
|
|
141
144
|
};
|
142
145
|
entityID: string;
|
143
146
|
thumbprint: string;
|
144
|
-
loginType: 'idp';
|
147
|
+
loginType: 'idp' | 'sp';
|
145
148
|
provider: string;
|
146
149
|
}
|
147
150
|
export interface SAMLConfig {
|
@@ -159,4 +162,10 @@ export interface ILogoutController {
|
|
159
162
|
}>;
|
160
163
|
handleResponse(body: SAMLResponsePayload): Promise<any>;
|
161
164
|
}
|
165
|
+
export interface OAuthErrorHandlerParams {
|
166
|
+
error: 'invalid_request' | 'access_denied' | 'unauthorized_client' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable';
|
167
|
+
error_description: string;
|
168
|
+
redirect_uri: string;
|
169
|
+
state?: string;
|
170
|
+
}
|
162
171
|
export {};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@boxyhq/saml-jackson",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.5",
|
4
4
|
"description": "SAML Jackson library",
|
5
5
|
"keywords": [
|
6
6
|
"SAML 2.0"
|
@@ -36,35 +36,36 @@
|
|
36
36
|
"statements": 70
|
37
37
|
},
|
38
38
|
"dependencies": {
|
39
|
-
"@boxyhq/saml20": "1.0.
|
39
|
+
"@boxyhq/saml20": "1.0.3",
|
40
40
|
"@opentelemetry/api-metrics": "0.27.0",
|
41
|
-
"@
|
42
|
-
"@peculiar/
|
43
|
-
"
|
41
|
+
"@opentelemetry/api": "1.0.4",
|
42
|
+
"@peculiar/webcrypto": "1.4.0",
|
43
|
+
"@peculiar/x509": "1.7.2",
|
44
|
+
"mongodb": "4.7.0",
|
44
45
|
"mysql2": "2.3.3",
|
45
46
|
"pg": "8.7.3",
|
46
47
|
"redis": "4.0.6",
|
47
48
|
"reflect-metadata": "0.1.13",
|
48
49
|
"ripemd160": "2.0.2",
|
49
|
-
"typeorm": "0.3.
|
50
|
+
"typeorm": "0.3.7",
|
50
51
|
"xml2js": "0.4.23",
|
51
52
|
"xmlbuilder": "15.1.1"
|
52
53
|
},
|
53
54
|
"devDependencies": {
|
54
|
-
"@types/node": "
|
55
|
-
"@types/sinon": "10.0.
|
55
|
+
"@types/node": "18.0.1",
|
56
|
+
"@types/sinon": "10.0.12",
|
56
57
|
"@types/tap": "15.0.7",
|
57
|
-
"@typescript-eslint/eslint-plugin": "5.
|
58
|
-
"@typescript-eslint/parser": "5.
|
58
|
+
"@typescript-eslint/eslint-plugin": "5.30.5",
|
59
|
+
"@typescript-eslint/parser": "5.30.5",
|
59
60
|
"cross-env": "7.0.3",
|
60
|
-
"eslint": "8.
|
61
|
+
"eslint": "8.19.0",
|
61
62
|
"eslint-config-prettier": "8.5.0",
|
62
|
-
"prettier": "2.
|
63
|
-
"sinon": "
|
64
|
-
"tap": "16.
|
65
|
-
"ts-node": "10.
|
66
|
-
"tsconfig-paths": "
|
67
|
-
"typescript": "4.
|
63
|
+
"prettier": "2.7.1",
|
64
|
+
"sinon": "14.0.0",
|
65
|
+
"tap": "16.3.0",
|
66
|
+
"ts-node": "10.8.2",
|
67
|
+
"tsconfig-paths": "4.0.0",
|
68
|
+
"typescript": "4.7.4"
|
68
69
|
},
|
69
70
|
"engines": {
|
70
71
|
"node": ">=14.18.1 <=16.x"
|