@boxyhq/saml-jackson 1.0.3 → 1.0.4
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 +127 -64
- package/dist/controller/utils.d.ts +3 -0
- package/dist/controller/utils.js +36 -1
- package/dist/typings.d.ts +7 -0
- package/package.json +8 -8
package/dist/controller/oauth.js
CHANGED
@@ -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,24 @@ 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
|
+
}),
|
241
|
+
};
|
242
|
+
}
|
226
243
|
let ssoUrl;
|
227
244
|
let post = false;
|
228
245
|
const { sso } = samlConfig.idpMetadata;
|
@@ -235,60 +252,80 @@ class OAuthController {
|
|
235
252
|
ssoUrl = sso.postUrl;
|
236
253
|
post = true;
|
237
254
|
}
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
const requested = { client_id, state };
|
247
|
-
if (requestedTenant) {
|
248
|
-
requested.tenant = requestedTenant;
|
249
|
-
}
|
250
|
-
if (requestedProduct) {
|
251
|
-
requested.product = requestedProduct;
|
255
|
+
else {
|
256
|
+
return {
|
257
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
258
|
+
error: 'invalid_request',
|
259
|
+
error_description: 'SAML binding could not be retrieved',
|
260
|
+
redirect_uri,
|
261
|
+
}),
|
262
|
+
};
|
252
263
|
}
|
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'),
|
264
|
+
try {
|
265
|
+
const samlReq = saml20_1.default.request({
|
266
|
+
ssoUrl,
|
267
|
+
entityID: this.opts.samlAudience,
|
268
|
+
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
|
269
|
+
signingKey: samlConfig.certs.privateKey,
|
270
|
+
publicKey: samlConfig.certs.publicKey,
|
273
271
|
});
|
272
|
+
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
273
|
+
const requested = { client_id, state };
|
274
|
+
if (requestedTenant) {
|
275
|
+
requested.tenant = requestedTenant;
|
276
|
+
}
|
277
|
+
if (requestedProduct) {
|
278
|
+
requested.product = requestedProduct;
|
279
|
+
}
|
280
|
+
if (idp_hint) {
|
281
|
+
requested.idp_hint = idp_hint;
|
282
|
+
}
|
283
|
+
yield this.sessionStore.put(sessionId, {
|
284
|
+
id: samlReq.id,
|
285
|
+
redirect_uri,
|
286
|
+
response_type,
|
287
|
+
state,
|
288
|
+
code_challenge,
|
289
|
+
code_challenge_method,
|
290
|
+
requested,
|
291
|
+
});
|
292
|
+
const relayState = utils_1.relayStatePrefix + sessionId;
|
293
|
+
let redirectUrl;
|
294
|
+
let authorizeForm;
|
295
|
+
if (!post) {
|
296
|
+
// HTTP Redirect binding
|
297
|
+
redirectUrl = redirect.success(ssoUrl, {
|
298
|
+
RelayState: relayState,
|
299
|
+
SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
|
300
|
+
});
|
301
|
+
}
|
302
|
+
else {
|
303
|
+
// HTTP POST binding
|
304
|
+
authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
|
305
|
+
{
|
306
|
+
name: 'RelayState',
|
307
|
+
value: relayState,
|
308
|
+
},
|
309
|
+
{
|
310
|
+
name: 'SAMLRequest',
|
311
|
+
value: Buffer.from(samlReq.request).toString('base64'),
|
312
|
+
},
|
313
|
+
]);
|
314
|
+
}
|
315
|
+
return {
|
316
|
+
redirect_url: redirectUrl,
|
317
|
+
authorize_form: authorizeForm,
|
318
|
+
};
|
319
|
+
}
|
320
|
+
catch (err) {
|
321
|
+
return {
|
322
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
323
|
+
error: 'server_error',
|
324
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
325
|
+
redirect_uri,
|
326
|
+
}),
|
327
|
+
};
|
274
328
|
}
|
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
329
|
});
|
293
330
|
}
|
294
331
|
samlResponse(body) {
|
@@ -346,10 +383,27 @@ class OAuthController {
|
|
346
383
|
thumbprint: samlConfig.idpMetadata.thumbprint,
|
347
384
|
audience: this.opts.samlAudience,
|
348
385
|
};
|
386
|
+
if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
|
387
|
+
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
388
|
+
}
|
349
389
|
if (session && session.id) {
|
350
390
|
validateOpts.inResponseTo = session.id;
|
351
391
|
}
|
352
|
-
|
392
|
+
let profile;
|
393
|
+
const redirect_uri = (session && session.redirect_uri) || samlConfig.defaultRedirectUrl;
|
394
|
+
try {
|
395
|
+
profile = yield validateResponse(rawResponse, validateOpts);
|
396
|
+
}
|
397
|
+
catch (err) {
|
398
|
+
// return error to redirect_uri
|
399
|
+
return {
|
400
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
401
|
+
error: 'access_denied',
|
402
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
403
|
+
redirect_uri,
|
404
|
+
}),
|
405
|
+
};
|
406
|
+
}
|
353
407
|
// store details against a code
|
354
408
|
const code = crypto_1.default.randomBytes(20).toString('hex');
|
355
409
|
const codeVal = {
|
@@ -361,9 +415,18 @@ class OAuthController {
|
|
361
415
|
if (session) {
|
362
416
|
codeVal.session = session;
|
363
417
|
}
|
364
|
-
|
365
|
-
|
366
|
-
|
418
|
+
try {
|
419
|
+
yield this.codeStore.put(code, codeVal);
|
420
|
+
}
|
421
|
+
catch (err) {
|
422
|
+
// return error to redirect_uri
|
423
|
+
return {
|
424
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
425
|
+
error: 'server_error',
|
426
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
427
|
+
redirect_uri,
|
428
|
+
}),
|
429
|
+
};
|
367
430
|
}
|
368
431
|
const params = {
|
369
432
|
code,
|
@@ -371,7 +434,7 @@ class OAuthController {
|
|
371
434
|
if (session && session.state) {
|
372
435
|
params.state = session.state;
|
373
436
|
}
|
374
|
-
const redirectUrl = redirect.success(
|
437
|
+
const redirectUrl = redirect.success(redirect_uri, params);
|
375
438
|
// delete the session
|
376
439
|
try {
|
377
440
|
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 }: 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 }) => {
|
45
|
+
return redirect.success(redirect_uri, { error, error_description });
|
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;
|
@@ -160,4 +162,9 @@ 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
|
+
}
|
163
170
|
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.4",
|
4
4
|
"description": "SAML Jackson library",
|
5
5
|
"keywords": [
|
6
6
|
"SAML 2.0"
|
@@ -38,12 +38,12 @@
|
|
38
38
|
"dependencies": {
|
39
39
|
"@boxyhq/saml20": "1.0.2",
|
40
40
|
"@opentelemetry/api-metrics": "0.27.0",
|
41
|
-
"@peculiar/webcrypto": "1.
|
42
|
-
"@peculiar/x509": "1.6.
|
43
|
-
"mongodb": "4.
|
41
|
+
"@peculiar/webcrypto": "1.4.0",
|
42
|
+
"@peculiar/x509": "1.6.3",
|
43
|
+
"mongodb": "4.6.0",
|
44
44
|
"mysql2": "2.3.3",
|
45
45
|
"pg": "8.7.3",
|
46
|
-
"redis": "4.
|
46
|
+
"redis": "4.0.6",
|
47
47
|
"reflect-metadata": "0.1.13",
|
48
48
|
"ripemd160": "2.0.2",
|
49
49
|
"typeorm": "0.3.6",
|
@@ -51,11 +51,11 @@
|
|
51
51
|
"xmlbuilder": "15.1.1"
|
52
52
|
},
|
53
53
|
"devDependencies": {
|
54
|
-
"@types/node": "17.0.
|
54
|
+
"@types/node": "17.0.34",
|
55
55
|
"@types/sinon": "10.0.11",
|
56
56
|
"@types/tap": "15.0.7",
|
57
|
-
"@typescript-eslint/eslint-plugin": "5.
|
58
|
-
"@typescript-eslint/parser": "5.
|
57
|
+
"@typescript-eslint/eslint-plugin": "5.25.0",
|
58
|
+
"@typescript-eslint/parser": "5.25.0",
|
59
59
|
"cross-env": "7.0.3",
|
60
60
|
"eslint": "8.15.0",
|
61
61
|
"eslint-config-prettier": "8.5.0",
|