@boxyhq/saml-jackson 1.0.3 → 1.0.6
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 +140 -67
- package/dist/controller/utils.d.ts +3 -0
- package/dist/controller/utils.js +36 -1
- package/dist/typings.d.ts +9 -1
- package/package.json +17 -16
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 getEncodedTenantProduct(
|
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, access_type, 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({
|
@@ -169,13 +166,15 @@ class OAuthController {
|
|
169
166
|
samlConfig = resolvedSamlConfig;
|
170
167
|
}
|
171
168
|
}
|
172
|
-
else if (
|
173
|
-
(access_type && access_type !== '' && access_type !== 'undefined' && access_type !== 'null')) {
|
169
|
+
else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
|
174
170
|
// if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
|
175
171
|
let sp = getEncodedTenantProduct(client_id);
|
176
172
|
if (!sp && access_type) {
|
177
173
|
sp = getEncodedTenantProduct(access_type);
|
178
174
|
}
|
175
|
+
if (!sp && scope) {
|
176
|
+
sp = getEncodedTenantProduct(scope);
|
177
|
+
}
|
179
178
|
if (sp && sp.tenant && sp.product) {
|
180
179
|
requestedTenant = sp.tenant;
|
181
180
|
requestedProduct = sp.product;
|
@@ -223,6 +222,25 @@ class OAuthController {
|
|
223
222
|
if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
|
224
223
|
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
225
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
|
+
}
|
226
244
|
let ssoUrl;
|
227
245
|
let post = false;
|
228
246
|
const { sso } = samlConfig.idpMetadata;
|
@@ -235,63 +253,86 @@ class OAuthController {
|
|
235
253
|
ssoUrl = sso.postUrl;
|
236
254
|
post = true;
|
237
255
|
}
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
if (requestedTenant) {
|
248
|
-
requested.tenant = requestedTenant;
|
249
|
-
}
|
250
|
-
if (requestedProduct) {
|
251
|
-
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
|
+
};
|
252
265
|
}
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
state,
|
261
|
-
code_challenge,
|
262
|
-
code_challenge_method,
|
263
|
-
requested,
|
264
|
-
});
|
265
|
-
const relayState = utils_1.relayStatePrefix + sessionId;
|
266
|
-
let redirectUrl;
|
267
|
-
let authorizeForm;
|
268
|
-
if (!post) {
|
269
|
-
// HTTP Redirect binding
|
270
|
-
redirectUrl = redirect.success(ssoUrl, {
|
271
|
-
RelayState: relayState,
|
272
|
-
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,
|
273
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
|
+
};
|
274
331
|
}
|
275
|
-
else {
|
276
|
-
// HTTP POST binding
|
277
|
-
authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
|
278
|
-
{
|
279
|
-
name: 'RelayState',
|
280
|
-
value: relayState,
|
281
|
-
},
|
282
|
-
{
|
283
|
-
name: 'SAMLRequest',
|
284
|
-
value: Buffer.from(samlReq.request).toString('base64'),
|
285
|
-
},
|
286
|
-
]);
|
287
|
-
}
|
288
|
-
return {
|
289
|
-
redirect_url: redirectUrl,
|
290
|
-
authorize_form: authorizeForm,
|
291
|
-
};
|
292
332
|
});
|
293
333
|
}
|
294
334
|
samlResponse(body) {
|
335
|
+
var _a, _b;
|
295
336
|
return __awaiter(this, void 0, void 0, function* () {
|
296
337
|
const { SAMLResponse, idp_hint } = body;
|
297
338
|
let RelayState = body.RelayState || ''; // RelayState will contain the sessionId from earlier quasi-oauth flow
|
@@ -302,10 +343,13 @@ class OAuthController {
|
|
302
343
|
}
|
303
344
|
RelayState = RelayState.replace(utils_1.relayStatePrefix, '');
|
304
345
|
const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
|
305
|
-
const
|
346
|
+
const issuer = saml20_1.default.parseIssuer(rawResponse);
|
347
|
+
if (!issuer) {
|
348
|
+
throw new error_1.JacksonError('Issuer not found.', 403);
|
349
|
+
}
|
306
350
|
const samlConfigs = yield this.configStore.getByIndex({
|
307
351
|
name: utils_1.IndexNames.EntityID,
|
308
|
-
value:
|
352
|
+
value: issuer,
|
309
353
|
});
|
310
354
|
if (!samlConfigs || samlConfigs.length === 0) {
|
311
355
|
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
@@ -345,11 +389,30 @@ class OAuthController {
|
|
345
389
|
const validateOpts = {
|
346
390
|
thumbprint: samlConfig.idpMetadata.thumbprint,
|
347
391
|
audience: this.opts.samlAudience,
|
392
|
+
privateKey: samlConfig.certs.privateKey,
|
348
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
|
+
}
|
349
397
|
if (session && session.id) {
|
350
398
|
validateOpts.inResponseTo = session.id;
|
351
399
|
}
|
352
|
-
|
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
|
+
}
|
353
416
|
// store details against a code
|
354
417
|
const code = crypto_1.default.randomBytes(20).toString('hex');
|
355
418
|
const codeVal = {
|
@@ -361,9 +424,19 @@ class OAuthController {
|
|
361
424
|
if (session) {
|
362
425
|
codeVal.session = session;
|
363
426
|
}
|
364
|
-
|
365
|
-
|
366
|
-
|
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
|
+
};
|
367
440
|
}
|
368
441
|
const params = {
|
369
442
|
code,
|
@@ -371,7 +444,7 @@ class OAuthController {
|
|
371
444
|
if (session && session.state) {
|
372
445
|
params.state = session.state;
|
373
446
|
}
|
374
|
-
const redirectUrl = redirect.success(
|
447
|
+
const redirectUrl = redirect.success(redirect_uri, params);
|
375
448
|
// delete the session
|
376
449
|
try {
|
377
450
|
yield this.sessionStore.delete(RelayState);
|
@@ -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
@@ -52,6 +52,7 @@ export interface OAuthReqBody {
|
|
52
52
|
tenant?: string;
|
53
53
|
product?: string;
|
54
54
|
access_type?: string;
|
55
|
+
scope?: string;
|
55
56
|
code_challenge: string;
|
56
57
|
code_challenge_method: 'plain' | 'S256' | '';
|
57
58
|
provider: 'saml';
|
@@ -79,6 +80,7 @@ export interface Profile {
|
|
79
80
|
email: string;
|
80
81
|
firstName: string;
|
81
82
|
lastName: string;
|
83
|
+
requested: Record<string, string>;
|
82
84
|
}
|
83
85
|
export interface Index {
|
84
86
|
name: string;
|
@@ -142,7 +144,7 @@ interface Metadata {
|
|
142
144
|
};
|
143
145
|
entityID: string;
|
144
146
|
thumbprint: string;
|
145
|
-
loginType: 'idp';
|
147
|
+
loginType: 'idp' | 'sp';
|
146
148
|
provider: string;
|
147
149
|
}
|
148
150
|
export interface SAMLConfig {
|
@@ -160,4 +162,10 @@ export interface ILogoutController {
|
|
160
162
|
}>;
|
161
163
|
handleResponse(body: SAMLResponsePayload): Promise<any>;
|
162
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
|
+
}
|
163
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.6",
|
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
|
-
"redis": "4.
|
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.3",
|
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
|
+
"prettier": "2.7.1",
|
63
64
|
"sinon": "14.0.0",
|
64
|
-
"tap": "16.
|
65
|
-
"ts-node": "10.
|
65
|
+
"tap": "16.3.0",
|
66
|
+
"ts-node": "10.8.2",
|
66
67
|
"tsconfig-paths": "4.0.0",
|
67
|
-
"typescript": "4.
|
68
|
+
"typescript": "4.7.4"
|
68
69
|
},
|
69
70
|
"engines": {
|
70
71
|
"node": ">=14.18.1 <=16.x"
|