@boxyhq/saml-jackson 0.5.0 → 1.0.1
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/README.md +1 -1
- package/dist/controller/api.d.ts +1 -0
- package/dist/controller/api.js +34 -2
- package/dist/controller/health-check.d.ts +11 -0
- package/dist/controller/health-check.js +53 -0
- package/dist/controller/oauth.js +26 -8
- package/dist/controller/signout.d.ts +18 -0
- package/dist/controller/signout.js +231 -0
- package/dist/controller/utils.d.ts +2 -1
- package/dist/controller/utils.js +13 -3
- package/dist/db/mem.js +31 -12
- package/dist/db/mongo.js +3 -10
- package/dist/db/redis.js +17 -6
- package/dist/db/sql/sql.d.ts +1 -1
- package/dist/db/sql/sql.js +10 -9
- package/dist/db/utils.d.ts +2 -0
- package/dist/db/utils.js +3 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +14 -2
- package/dist/saml/saml.d.ts +3 -0
- package/dist/saml/saml.js +33 -4
- package/dist/typings.d.ts +43 -1
- package/package.json +14 -14
package/README.md
CHANGED
@@ -14,7 +14,7 @@ npm i @boxyhq/saml-jackson
|
|
14
14
|
|
15
15
|
## Documentation
|
16
16
|
|
17
|
-
For full documentation, visit [boxyhq.com/docs/jackson/npm-library](https://boxyhq.com/docs/jackson/npm-library)
|
17
|
+
For full documentation, visit [boxyhq.com/docs/jackson/deploy/npm-library](https://boxyhq.com/docs/jackson/deploy/npm-library)
|
18
18
|
|
19
19
|
## License
|
20
20
|
|
package/dist/controller/api.d.ts
CHANGED
package/dist/controller/api.js
CHANGED
@@ -58,6 +58,19 @@ class APIController {
|
|
58
58
|
constructor({ configStore }) {
|
59
59
|
this.configStore = configStore;
|
60
60
|
}
|
61
|
+
_validateRedirectUrl({ redirectUrlList, defaultRedirectUrl }) {
|
62
|
+
if (redirectUrlList) {
|
63
|
+
if (redirectUrlList.length > 100) {
|
64
|
+
throw new error_1.JacksonError('Exceeded maximum number of allowed redirect urls', 400);
|
65
|
+
}
|
66
|
+
for (const url of redirectUrlList) {
|
67
|
+
(0, utils_1.validateAbsoluteUrl)(url, 'redirectUrl is invalid');
|
68
|
+
}
|
69
|
+
}
|
70
|
+
if (defaultRedirectUrl) {
|
71
|
+
(0, utils_1.validateAbsoluteUrl)(defaultRedirectUrl, 'defaultRedirectUrl is invalid');
|
72
|
+
}
|
73
|
+
}
|
61
74
|
_validateIdPConfig(body) {
|
62
75
|
const { encodedRawMetadata, rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product, description } = body;
|
63
76
|
if (!rawMetadata && !encodedRawMetadata) {
|
@@ -168,6 +181,8 @@ class APIController {
|
|
168
181
|
const { encodedRawMetadata, rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product, name, description, } = body;
|
169
182
|
metrics.increment('createConfig');
|
170
183
|
this._validateIdPConfig(body);
|
184
|
+
const redirectUrlList = extractRedirectUrls(redirectUrl);
|
185
|
+
this._validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
171
186
|
let metaData = rawMetadata;
|
172
187
|
if (encodedRawMetadata) {
|
173
188
|
metaData = Buffer.from(encodedRawMetadata, 'base64').toString();
|
@@ -195,7 +210,7 @@ class APIController {
|
|
195
210
|
const record = {
|
196
211
|
idpMetadata,
|
197
212
|
defaultRedirectUrl,
|
198
|
-
redirectUrl:
|
213
|
+
redirectUrl: redirectUrlList,
|
199
214
|
tenant,
|
200
215
|
product,
|
201
216
|
name,
|
@@ -296,6 +311,8 @@ class APIController {
|
|
296
311
|
if (description && description.length > 100) {
|
297
312
|
throw new error_1.JacksonError('Description should not exceed 100 characters', 400);
|
298
313
|
}
|
314
|
+
const redirectUrlList = redirectUrl ? extractRedirectUrls(redirectUrl) : null;
|
315
|
+
this._validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
299
316
|
const _currentConfig = yield this.getConfig(clientInfo);
|
300
317
|
if (_currentConfig.clientSecret !== (clientInfo === null || clientInfo === void 0 ? void 0 : clientInfo.clientSecret)) {
|
301
318
|
throw new error_1.JacksonError('clientSecret mismatch', 400);
|
@@ -321,7 +338,7 @@ class APIController {
|
|
321
338
|
throw new error_1.JacksonError('Tenant/Product config mismatch with IdP metadata', 400);
|
322
339
|
}
|
323
340
|
}
|
324
|
-
const record = Object.assign(Object.assign({}, _currentConfig), { name: name ? name : _currentConfig.name, description: description ? description : _currentConfig.description, idpMetadata: newMetadata ? newMetadata : _currentConfig.idpMetadata, defaultRedirectUrl: defaultRedirectUrl ? defaultRedirectUrl : _currentConfig.defaultRedirectUrl, redirectUrl:
|
341
|
+
const record = Object.assign(Object.assign({}, _currentConfig), { name: name ? name : _currentConfig.name, description: description ? description : _currentConfig.description, idpMetadata: newMetadata ? newMetadata : _currentConfig.idpMetadata, defaultRedirectUrl: defaultRedirectUrl ? defaultRedirectUrl : _currentConfig.defaultRedirectUrl, redirectUrl: redirectUrlList ? redirectUrlList : _currentConfig.redirectUrl });
|
325
342
|
yield this.configStore.put(clientInfo === null || clientInfo === void 0 ? void 0 : clientInfo.clientID, record, {
|
326
343
|
// secondary index on entityID
|
327
344
|
name: utils_1.IndexNames.EntityID,
|
@@ -495,3 +512,18 @@ const extractHostName = (url) => {
|
|
495
512
|
return null;
|
496
513
|
}
|
497
514
|
};
|
515
|
+
const extractRedirectUrls = (urls) => {
|
516
|
+
if (!urls) {
|
517
|
+
return [];
|
518
|
+
}
|
519
|
+
if (typeof urls === 'string') {
|
520
|
+
if (urls.startsWith('[')) {
|
521
|
+
// redirectUrl is a stringified array
|
522
|
+
return JSON.parse(urls);
|
523
|
+
}
|
524
|
+
// redirectUrl is a single URL
|
525
|
+
return [urls];
|
526
|
+
}
|
527
|
+
// redirectUrl is an array of URLs
|
528
|
+
return urls;
|
529
|
+
};
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { IHealthCheckController, Storable } from '../typings';
|
2
|
+
export declare class HealthCheckController implements IHealthCheckController {
|
3
|
+
healthCheckStore: Storable;
|
4
|
+
constructor({ healthCheckStore }: {
|
5
|
+
healthCheckStore: any;
|
6
|
+
});
|
7
|
+
init(): Promise<void>;
|
8
|
+
status(): Promise<{
|
9
|
+
status: number;
|
10
|
+
}>;
|
11
|
+
}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
|
+
});
|
10
|
+
};
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
12
|
+
exports.HealthCheckController = void 0;
|
13
|
+
const error_1 = require("./error");
|
14
|
+
const healthKey = 'amihealthy';
|
15
|
+
const healthValue = 'fit';
|
16
|
+
const g = global;
|
17
|
+
class HealthCheckController {
|
18
|
+
constructor({ healthCheckStore }) {
|
19
|
+
this.healthCheckStore = healthCheckStore;
|
20
|
+
}
|
21
|
+
init() {
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
23
|
+
this.healthCheckStore.put(healthKey, healthValue);
|
24
|
+
});
|
25
|
+
}
|
26
|
+
status() {
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
28
|
+
try {
|
29
|
+
if (!g.isJacksonReady) {
|
30
|
+
return {
|
31
|
+
status: 503,
|
32
|
+
};
|
33
|
+
}
|
34
|
+
const response = yield Promise.race([
|
35
|
+
this.healthCheckStore.get(healthKey),
|
36
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1000)),
|
37
|
+
]);
|
38
|
+
if (response === healthValue) {
|
39
|
+
return {
|
40
|
+
status: 200,
|
41
|
+
};
|
42
|
+
}
|
43
|
+
return {
|
44
|
+
status: 503,
|
45
|
+
};
|
46
|
+
}
|
47
|
+
catch (err) {
|
48
|
+
throw new error_1.JacksonError('Service not available', 503);
|
49
|
+
}
|
50
|
+
});
|
51
|
+
}
|
52
|
+
}
|
53
|
+
exports.HealthCheckController = HealthCheckController;
|
package/dist/controller/oauth.js
CHANGED
@@ -79,6 +79,8 @@ class OAuthController {
|
|
79
79
|
const { response_type = 'code', client_id, redirect_uri, state, tenant, product, code_challenge, code_challenge_method = '',
|
80
80
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
81
81
|
provider = 'saml', } = body;
|
82
|
+
let requestedTenant = tenant;
|
83
|
+
let requestedProduct = product;
|
82
84
|
metrics.increment('oauthAuthorize');
|
83
85
|
if (!redirect_uri) {
|
84
86
|
throw new error_1.JacksonError('Please specify a redirect URL.', 400);
|
@@ -102,6 +104,8 @@ class OAuthController {
|
|
102
104
|
// if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
|
103
105
|
const sp = getEncodedClientId(client_id);
|
104
106
|
if (sp === null || sp === void 0 ? void 0 : sp.tenant) {
|
107
|
+
requestedTenant = sp.tenant;
|
108
|
+
requestedProduct = sp.product || '';
|
105
109
|
const samlConfigs = yield this.configStore.getByIndex({
|
106
110
|
name: utils_1.IndexNames.TenantProduct,
|
107
111
|
value: dbutils.keyFromParts(sp.tenant, sp.product || ''),
|
@@ -145,9 +149,9 @@ class OAuthController {
|
|
145
149
|
publicKey: samlConfig.certs.publicKey,
|
146
150
|
});
|
147
151
|
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
148
|
-
const
|
149
|
-
tenant,
|
150
|
-
product,
|
152
|
+
const requested = {
|
153
|
+
tenant: requestedTenant,
|
154
|
+
product: requestedProduct,
|
151
155
|
client_id,
|
152
156
|
state,
|
153
157
|
};
|
@@ -158,7 +162,7 @@ class OAuthController {
|
|
158
162
|
state,
|
159
163
|
code_challenge,
|
160
164
|
code_challenge_method,
|
161
|
-
requested
|
165
|
+
requested,
|
162
166
|
});
|
163
167
|
const relayState = relayStatePrefix + sessionId;
|
164
168
|
let redirectUrl;
|
@@ -172,7 +176,7 @@ class OAuthController {
|
|
172
176
|
}
|
173
177
|
else {
|
174
178
|
// HTTP POST binding
|
175
|
-
authorizeForm = (0, utils_1.
|
179
|
+
authorizeForm = (0, utils_1.createRequestForm)(relayState, encodeURI(Buffer.from(samlReq.request).toString('base64')), ssoUrl);
|
176
180
|
}
|
177
181
|
return {
|
178
182
|
redirect_url: redirectUrl,
|
@@ -201,8 +205,6 @@ class OAuthController {
|
|
201
205
|
if (!samlConfigs || samlConfigs.length === 0) {
|
202
206
|
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
203
207
|
}
|
204
|
-
// TODO: Support multiple matches
|
205
|
-
const samlConfig = samlConfigs[0];
|
206
208
|
let session;
|
207
209
|
if (RelayState !== '') {
|
208
210
|
session = yield this.sessionStore.get(RelayState);
|
@@ -210,6 +212,17 @@ class OAuthController {
|
|
210
212
|
throw new error_1.JacksonError('Unable to validate state from the origin request.', 403);
|
211
213
|
}
|
212
214
|
}
|
215
|
+
// Resolve if there are multiple matches for SP login. TODO: Support multiple matches for IdP login
|
216
|
+
const samlConfig = samlConfigs.length === 1
|
217
|
+
? samlConfigs[0]
|
218
|
+
: samlConfigs.filter((c) => {
|
219
|
+
var _a, _b, _c;
|
220
|
+
return (c.clientID === ((_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.client_id) ||
|
221
|
+
(c.tenant === ((_b = session === null || session === void 0 ? void 0 : session.requested) === null || _b === void 0 ? void 0 : _b.tenant) && c.product === ((_c = session === null || session === void 0 ? void 0 : session.requested) === null || _c === void 0 ? void 0 : _c.product)));
|
222
|
+
})[0];
|
223
|
+
if (!samlConfig) {
|
224
|
+
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
225
|
+
}
|
213
226
|
const validateOpts = {
|
214
227
|
thumbprint: samlConfig.idpMetadata.thumbprint,
|
215
228
|
audience: this.opts.samlAudience,
|
@@ -224,7 +237,7 @@ class OAuthController {
|
|
224
237
|
profile,
|
225
238
|
clientID: samlConfig.clientID,
|
226
239
|
clientSecret: samlConfig.clientSecret,
|
227
|
-
requested: session.requested,
|
240
|
+
requested: session === null || session === void 0 ? void 0 : session.requested,
|
228
241
|
};
|
229
242
|
if (session) {
|
230
243
|
codeVal.session = session;
|
@@ -346,6 +359,11 @@ class OAuthController {
|
|
346
359
|
}
|
347
360
|
}
|
348
361
|
}
|
362
|
+
else {
|
363
|
+
if (client_secret !== this.opts.clientSecretVerifier && client_secret !== codeVal.clientSecret) {
|
364
|
+
throw new error_1.JacksonError('Invalid client_secret', 401);
|
365
|
+
}
|
366
|
+
}
|
349
367
|
}
|
350
368
|
else if (codeVal && codeVal.session) {
|
351
369
|
throw new error_1.JacksonError('Please specify client_secret or code_verifier', 401);
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { SAMLResponsePayload, SLORequestParams } from '../typings';
|
2
|
+
export declare class LogoutController {
|
3
|
+
private configStore;
|
4
|
+
private sessionStore;
|
5
|
+
private opts;
|
6
|
+
constructor({ configStore, sessionStore, opts }: {
|
7
|
+
configStore: any;
|
8
|
+
sessionStore: any;
|
9
|
+
opts: any;
|
10
|
+
});
|
11
|
+
createRequest({ nameId, tenant, product, redirectUrl }: SLORequestParams): Promise<{
|
12
|
+
logoutUrl: string | null;
|
13
|
+
logoutForm: string | null;
|
14
|
+
}>;
|
15
|
+
handleResponse({ SAMLResponse, RelayState }: SAMLResponsePayload): Promise<{
|
16
|
+
redirectUrl: any;
|
17
|
+
}>;
|
18
|
+
}
|
@@ -0,0 +1,231 @@
|
|
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
|
+
};
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
32
|
+
});
|
33
|
+
};
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
36
|
+
};
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
38
|
+
exports.LogoutController = void 0;
|
39
|
+
const xmldom_1 = require("@xmldom/xmldom");
|
40
|
+
const crypto_1 = __importDefault(require("crypto"));
|
41
|
+
const thumbprint_1 = __importDefault(require("thumbprint"));
|
42
|
+
const util_1 = require("util");
|
43
|
+
const xml_crypto_1 = require("xml-crypto");
|
44
|
+
const xml2js_1 = __importDefault(require("xml2js"));
|
45
|
+
const xmlbuilder_1 = __importDefault(require("xmlbuilder"));
|
46
|
+
const zlib_1 = require("zlib");
|
47
|
+
const dbutils = __importStar(require("../db/utils"));
|
48
|
+
const saml_1 = __importDefault(require("../saml/saml"));
|
49
|
+
const error_1 = require("./error");
|
50
|
+
const redirect = __importStar(require("./oauth/redirect"));
|
51
|
+
const utils_1 = require("./utils");
|
52
|
+
const deflateRawAsync = (0, util_1.promisify)(zlib_1.deflateRaw);
|
53
|
+
const relayStatePrefix = 'boxyhq_jackson_';
|
54
|
+
class LogoutController {
|
55
|
+
constructor({ configStore, sessionStore, opts }) {
|
56
|
+
this.opts = opts;
|
57
|
+
this.configStore = configStore;
|
58
|
+
this.sessionStore = sessionStore;
|
59
|
+
}
|
60
|
+
// Create SLO Request
|
61
|
+
createRequest({ nameId, tenant, product, redirectUrl }) {
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
63
|
+
let samlConfig = null;
|
64
|
+
if (tenant && product) {
|
65
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
66
|
+
name: utils_1.IndexNames.TenantProduct,
|
67
|
+
value: dbutils.keyFromParts(tenant, product),
|
68
|
+
});
|
69
|
+
if (!samlConfigs || samlConfigs.length === 0) {
|
70
|
+
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
71
|
+
}
|
72
|
+
samlConfig = samlConfigs[0];
|
73
|
+
}
|
74
|
+
if (!samlConfig) {
|
75
|
+
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
76
|
+
}
|
77
|
+
const { idpMetadata: { slo, provider }, certs: { privateKey, publicKey }, } = samlConfig;
|
78
|
+
if ('redirectUrl' in slo === false && 'postUrl' in slo === false) {
|
79
|
+
throw new error_1.JacksonError(`${provider} doesn't support SLO or disabled by IdP.`, 400);
|
80
|
+
}
|
81
|
+
const { id, xml } = buildRequestXML(nameId, this.opts.samlAudience, slo.redirectUrl);
|
82
|
+
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
83
|
+
let logoutUrl = null;
|
84
|
+
let logoutForm = null;
|
85
|
+
const relayState = relayStatePrefix + sessionId;
|
86
|
+
const signedXML = yield signXML(xml, privateKey, publicKey);
|
87
|
+
yield this.sessionStore.put(sessionId, {
|
88
|
+
id,
|
89
|
+
redirectUrl,
|
90
|
+
});
|
91
|
+
// HTTP-Redirect binding
|
92
|
+
if ('redirectUrl' in slo) {
|
93
|
+
logoutUrl = redirect.success(slo.redirectUrl, {
|
94
|
+
SAMLRequest: Buffer.from(yield deflateRawAsync(signedXML)).toString('base64'),
|
95
|
+
RelayState: relayState,
|
96
|
+
});
|
97
|
+
}
|
98
|
+
// HTTP-POST binding
|
99
|
+
if ('postUrl' in slo) {
|
100
|
+
logoutForm = (0, utils_1.createRequestForm)(relayState, encodeURI(Buffer.from(signedXML).toString('base64')), slo.postUrl);
|
101
|
+
}
|
102
|
+
return { logoutUrl, logoutForm };
|
103
|
+
});
|
104
|
+
}
|
105
|
+
// Handle SLO Response
|
106
|
+
handleResponse({ SAMLResponse, RelayState }) {
|
107
|
+
var _a;
|
108
|
+
return __awaiter(this, void 0, void 0, function* () {
|
109
|
+
const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
|
110
|
+
const sessionId = RelayState.replace(relayStatePrefix, '');
|
111
|
+
const session = yield this.sessionStore.get(sessionId);
|
112
|
+
if (!session) {
|
113
|
+
throw new error_1.JacksonError('Unable to validate state from the origin request.', 403);
|
114
|
+
}
|
115
|
+
const parsedResponse = yield parseSAMLResponse(rawResponse);
|
116
|
+
if (parsedResponse.status !== 'urn:oasis:names:tc:SAML:2.0:status:Success') {
|
117
|
+
throw new error_1.JacksonError(`SLO failed with status ${parsedResponse.status}.`, 400);
|
118
|
+
}
|
119
|
+
if (parsedResponse.inResponseTo !== session.id) {
|
120
|
+
throw new error_1.JacksonError(`SLO failed with mismatched request ID.`, 400);
|
121
|
+
}
|
122
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
123
|
+
name: utils_1.IndexNames.EntityID,
|
124
|
+
value: parsedResponse.issuer,
|
125
|
+
});
|
126
|
+
if (!samlConfigs || samlConfigs.length === 0) {
|
127
|
+
throw new error_1.JacksonError('SAML configuration not found.', 403);
|
128
|
+
}
|
129
|
+
const { idpMetadata, defaultRedirectUrl } = samlConfigs[0];
|
130
|
+
if (!(yield hasValidSignature(rawResponse, idpMetadata.thumbprint))) {
|
131
|
+
throw new error_1.JacksonError('Invalid signature.', 403);
|
132
|
+
}
|
133
|
+
try {
|
134
|
+
yield this.sessionStore.delete(sessionId);
|
135
|
+
}
|
136
|
+
catch (_err) {
|
137
|
+
// Ignore
|
138
|
+
}
|
139
|
+
return {
|
140
|
+
redirectUrl: (_a = session.redirectUrl) !== null && _a !== void 0 ? _a : defaultRedirectUrl,
|
141
|
+
};
|
142
|
+
});
|
143
|
+
}
|
144
|
+
}
|
145
|
+
exports.LogoutController = LogoutController;
|
146
|
+
// Create the XML for the SLO Request
|
147
|
+
const buildRequestXML = (nameId, providerName, sloUrl) => {
|
148
|
+
const id = '_' + crypto_1.default.randomBytes(10).toString('hex');
|
149
|
+
const xml = {
|
150
|
+
'samlp:LogoutRequest': {
|
151
|
+
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
152
|
+
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
|
153
|
+
'@ID': id,
|
154
|
+
'@Version': '2.0',
|
155
|
+
'@IssueInstant': new Date().toISOString(),
|
156
|
+
'@Destination': sloUrl,
|
157
|
+
'saml:Issuer': {
|
158
|
+
'#text': providerName,
|
159
|
+
},
|
160
|
+
'saml:NameID': {
|
161
|
+
'@Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
|
162
|
+
'#text': nameId,
|
163
|
+
},
|
164
|
+
},
|
165
|
+
};
|
166
|
+
return {
|
167
|
+
id,
|
168
|
+
xml: xmlbuilder_1.default.create(xml).end({}),
|
169
|
+
};
|
170
|
+
};
|
171
|
+
// Parse SAMLResponse
|
172
|
+
const parseSAMLResponse = (rawResponse) => __awaiter(void 0, void 0, void 0, function* () {
|
173
|
+
return new Promise((resolve, reject) => {
|
174
|
+
xml2js_1.default.parseString(rawResponse, { tagNameProcessors: [xml2js_1.default.processors.stripPrefix] }, (err, { LogoutResponse }) => {
|
175
|
+
if (err) {
|
176
|
+
reject(err);
|
177
|
+
return;
|
178
|
+
}
|
179
|
+
resolve({
|
180
|
+
issuer: LogoutResponse.Issuer[0]._,
|
181
|
+
id: LogoutResponse.$.ID,
|
182
|
+
status: LogoutResponse.Status[0].StatusCode[0].$.Value,
|
183
|
+
destination: LogoutResponse.$.Destination,
|
184
|
+
inResponseTo: LogoutResponse.$.InResponseTo,
|
185
|
+
});
|
186
|
+
});
|
187
|
+
});
|
188
|
+
});
|
189
|
+
// Sign the XML
|
190
|
+
const signXML = (xml, signingKey, publicKey) => __awaiter(void 0, void 0, void 0, function* () {
|
191
|
+
const sig = new xml_crypto_1.SignedXml();
|
192
|
+
sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
|
193
|
+
sig.keyInfoProvider = new saml_1.default.PubKeyInfo(publicKey);
|
194
|
+
sig.signingKey = signingKey;
|
195
|
+
sig.addReference("/*[local-name(.)='LogoutRequest']", ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#'], 'http://www.w3.org/2001/04/xmlenc#sha256');
|
196
|
+
sig.computeSignature(xml);
|
197
|
+
return sig.getSignedXml();
|
198
|
+
});
|
199
|
+
// Validate signature
|
200
|
+
const hasValidSignature = (xml, certThumbprint) => __awaiter(void 0, void 0, void 0, function* () {
|
201
|
+
return new Promise((resolve, reject) => {
|
202
|
+
const doc = new xmldom_1.DOMParser().parseFromString(xml);
|
203
|
+
const signed = new xml_crypto_1.SignedXml();
|
204
|
+
let calculatedThumbprint;
|
205
|
+
const signature = (0, xml_crypto_1.xpath)(doc, "/*/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0] ||
|
206
|
+
(0, xml_crypto_1.xpath)(doc, "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0] ||
|
207
|
+
(0, xml_crypto_1.xpath)(doc, "/*/*/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];
|
208
|
+
signed.keyInfoProvider = {
|
209
|
+
getKey: function getKey(keyInfo) {
|
210
|
+
if (certThumbprint) {
|
211
|
+
const embeddedSignature = keyInfo[0].getElementsByTagNameNS('http://www.w3.org/2000/09/xmldsig#', 'X509Certificate');
|
212
|
+
if (embeddedSignature.length > 0) {
|
213
|
+
const base64cer = embeddedSignature[0].firstChild.toString();
|
214
|
+
calculatedThumbprint = thumbprint_1.default.calculate(base64cer);
|
215
|
+
return saml_1.default.certToPEM(base64cer);
|
216
|
+
}
|
217
|
+
}
|
218
|
+
},
|
219
|
+
getKeyInfo: function getKeyInfo() {
|
220
|
+
return '<X509Data></X509Data>';
|
221
|
+
},
|
222
|
+
};
|
223
|
+
signed.loadSignature(signature.toString());
|
224
|
+
try {
|
225
|
+
return resolve(signed.checkSignature(xml) && calculatedThumbprint.toUpperCase() === certThumbprint.toUpperCase());
|
226
|
+
}
|
227
|
+
catch (err) {
|
228
|
+
return reject(err);
|
229
|
+
}
|
230
|
+
});
|
231
|
+
});
|
@@ -2,4 +2,5 @@ export declare enum IndexNames {
|
|
2
2
|
EntityID = "entityID",
|
3
3
|
TenantProduct = "tenantProduct"
|
4
4
|
}
|
5
|
-
export declare const
|
5
|
+
export declare const createRequestForm: (relayState: string, samlReqEnc: string, postUrl: string) => string;
|
6
|
+
export declare const validateAbsoluteUrl: (url: any, message: any) => void;
|
package/dist/controller/utils.js
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.
|
3
|
+
exports.validateAbsoluteUrl = exports.createRequestForm = exports.IndexNames = void 0;
|
4
|
+
const error_1 = require("./error");
|
4
5
|
var IndexNames;
|
5
6
|
(function (IndexNames) {
|
6
7
|
IndexNames["EntityID"] = "entityID";
|
7
8
|
IndexNames["TenantProduct"] = "tenantProduct";
|
8
9
|
})(IndexNames = exports.IndexNames || (exports.IndexNames = {}));
|
9
|
-
const
|
10
|
+
const createRequestForm = (relayState, samlReqEnc, postUrl) => {
|
10
11
|
const formElements = [
|
11
12
|
'<!DOCTYPE html>',
|
12
13
|
'<html>',
|
@@ -29,4 +30,13 @@ const createAuthorizeForm = (relayState, samlReqEnc, postUrl) => {
|
|
29
30
|
];
|
30
31
|
return formElements.join('');
|
31
32
|
};
|
32
|
-
exports.
|
33
|
+
exports.createRequestForm = createRequestForm;
|
34
|
+
const validateAbsoluteUrl = (url, message) => {
|
35
|
+
try {
|
36
|
+
new URL(url);
|
37
|
+
}
|
38
|
+
catch (err) {
|
39
|
+
throw new error_1.JacksonError(message ? message : 'Invalid url', 400);
|
40
|
+
}
|
41
|
+
};
|
42
|
+
exports.validateAbsoluteUrl = validateAbsoluteUrl;
|
package/dist/db/mem.js
CHANGED
@@ -79,16 +79,16 @@ class Mem {
|
|
79
79
|
take += skip;
|
80
80
|
const returnValue = [];
|
81
81
|
if (namespace) {
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
count++;
|
82
|
+
const val = Array.from(this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)]);
|
83
|
+
const iterator = val.reverse().values();
|
84
|
+
for (const value of iterator) {
|
85
|
+
if (count >= take) {
|
86
|
+
break;
|
87
|
+
}
|
88
|
+
if (count >= skip) {
|
89
|
+
returnValue.push(this.store[dbutils.keyFromParts(namespace, value)]);
|
91
90
|
}
|
91
|
+
count++;
|
92
92
|
}
|
93
93
|
}
|
94
94
|
return returnValue || [];
|
@@ -108,9 +108,6 @@ class Mem {
|
|
108
108
|
return __awaiter(this, void 0, void 0, function* () {
|
109
109
|
const k = dbutils.key(namespace, key);
|
110
110
|
this.store[k] = val;
|
111
|
-
if (!Date.parse(this.store['createdAt']))
|
112
|
-
this.store['createdAt'] = new Date().toISOString();
|
113
|
-
this.store['modifiedAt'] = new Date().toISOString();
|
114
111
|
// console.log(this.store)
|
115
112
|
if (ttl) {
|
116
113
|
this.ttlStore[k] = {
|
@@ -136,6 +133,26 @@ class Mem {
|
|
136
133
|
}
|
137
134
|
cleanup.add(idxKey);
|
138
135
|
}
|
136
|
+
let createdAtSet = this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)];
|
137
|
+
if (!createdAtSet) {
|
138
|
+
createdAtSet = new Set();
|
139
|
+
this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)] = createdAtSet;
|
140
|
+
this.store['createdAt'] = new Date().toISOString();
|
141
|
+
createdAtSet.add(key);
|
142
|
+
}
|
143
|
+
else {
|
144
|
+
if (!this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)].has(key)) {
|
145
|
+
createdAtSet.add(key);
|
146
|
+
this.store['createdAt'] = new Date().toISOString();
|
147
|
+
}
|
148
|
+
}
|
149
|
+
let modifiedAtSet = this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)];
|
150
|
+
if (!modifiedAtSet) {
|
151
|
+
modifiedAtSet = new Set();
|
152
|
+
this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)] = modifiedAtSet;
|
153
|
+
}
|
154
|
+
modifiedAtSet.add(key);
|
155
|
+
this.store['modifiedAt'] = new Date().toISOString();
|
139
156
|
});
|
140
157
|
}
|
141
158
|
delete(namespace, key) {
|
@@ -148,6 +165,8 @@ class Mem {
|
|
148
165
|
for (const dbKey of dbKeys || []) {
|
149
166
|
this.indexes[dbKey] && this.indexes[dbKey].delete(key);
|
150
167
|
}
|
168
|
+
this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)].delete(key);
|
169
|
+
this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)].delete(key);
|
151
170
|
delete this.cleanup[idxKey];
|
152
171
|
delete this.ttlStore[k];
|
153
172
|
});
|
package/dist/db/mongo.js
CHANGED
@@ -40,16 +40,9 @@ class Mongo {
|
|
40
40
|
}
|
41
41
|
init() {
|
42
42
|
return __awaiter(this, void 0, void 0, function* () {
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
}
|
47
|
-
this.client = new mongodb_1.MongoClient(this.options.url);
|
48
|
-
yield this.client.connect();
|
49
|
-
}
|
50
|
-
catch (err) {
|
51
|
-
console.error(`error connecting to ${this.options.type} db: ${err}`);
|
52
|
-
}
|
43
|
+
const dbUrl = this.options.url;
|
44
|
+
this.client = new mongodb_1.MongoClient(dbUrl);
|
45
|
+
yield this.client.connect();
|
53
46
|
this.db = this.client.db();
|
54
47
|
this.collection = this.db.collection('jacksonStore');
|
55
48
|
yield this.collection.createIndex({ indexes: 1 });
|
package/dist/db/redis.js
CHANGED
@@ -79,16 +79,13 @@ class Redis {
|
|
79
79
|
let count = 0;
|
80
80
|
take += skip;
|
81
81
|
try {
|
82
|
-
for (var _b = __asyncValues(this.client.
|
83
|
-
|
84
|
-
COUNT: Math.min(take, 1000),
|
85
|
-
})), _c; _c = yield _b.next(), !_c.done;) {
|
86
|
-
const key = _c.value;
|
82
|
+
for (var _b = __asyncValues(this.client.zScanIterator(dbutils.keyFromParts(dbutils.createdAtPrefix, namespace), Math.min(take, 1000))), _c; _c = yield _b.next(), !_c.done;) {
|
83
|
+
const { score, value } = _c.value;
|
87
84
|
if (count >= take) {
|
88
85
|
break;
|
89
86
|
}
|
90
87
|
if (count >= skip) {
|
91
|
-
keyArray.push(
|
88
|
+
keyArray.push(dbutils.keyFromParts(namespace, value));
|
92
89
|
}
|
93
90
|
count++;
|
94
91
|
}
|
@@ -137,6 +134,18 @@ class Redis {
|
|
137
134
|
tx = tx.sAdd(dbutils.keyFromParts(dbutils.indexPrefix, idxKey), key);
|
138
135
|
tx = tx.sAdd(dbutils.keyFromParts(dbutils.indexPrefix, k), idxKey);
|
139
136
|
}
|
137
|
+
const timestamp = Number(Date.now());
|
138
|
+
//Converting Timestamp in negative so that when we get the value, it will be found in reverse order (descending order).
|
139
|
+
const negativeTimestamp = -Math.abs(timestamp);
|
140
|
+
const value = yield this.client.get(k);
|
141
|
+
if (!value) {
|
142
|
+
tx = tx.zAdd(dbutils.keyFromParts(dbutils.createdAtPrefix, namespace), [
|
143
|
+
{ score: negativeTimestamp, value: key },
|
144
|
+
]);
|
145
|
+
}
|
146
|
+
tx = tx.zAdd(dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace), [
|
147
|
+
{ score: negativeTimestamp, value: key },
|
148
|
+
]);
|
140
149
|
yield tx.exec();
|
141
150
|
});
|
142
151
|
}
|
@@ -151,6 +160,8 @@ class Redis {
|
|
151
160
|
for (const dbKey of dbKeys || []) {
|
152
161
|
tx.sRem(dbutils.keyFromParts(dbutils.indexPrefix, dbKey), key);
|
153
162
|
}
|
163
|
+
tx.ZREM(dbutils.keyFromParts(dbutils.createdAtPrefix, namespace), key);
|
164
|
+
tx.ZREM(dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace), key);
|
154
165
|
tx.del(idxKey);
|
155
166
|
return yield tx.exec();
|
156
167
|
});
|
package/dist/db/sql/sql.d.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import { DatabaseDriver, DatabaseOption, Index, Encrypted } from '../../typings';
|
2
2
|
declare class Sql implements DatabaseDriver {
|
3
3
|
private options;
|
4
|
-
private
|
4
|
+
private dataSource;
|
5
5
|
private storeRepository;
|
6
6
|
private indexRepository;
|
7
7
|
private ttlRepository;
|
package/dist/db/sql/sql.js
CHANGED
@@ -47,8 +47,8 @@ class Sql {
|
|
47
47
|
return __awaiter(this, void 0, void 0, function* () {
|
48
48
|
while (true) {
|
49
49
|
try {
|
50
|
-
this.
|
51
|
-
name: this.options.type + Math.floor(Math.random() * 100000),
|
50
|
+
this.dataSource = new typeorm_1.DataSource({
|
51
|
+
// name: this.options.type! + Math.floor(Math.random() * 100000),
|
52
52
|
type: this.options.type,
|
53
53
|
url: this.options.url,
|
54
54
|
synchronize: true,
|
@@ -56,6 +56,7 @@ class Sql {
|
|
56
56
|
logging: ['error'],
|
57
57
|
entities: [JacksonStore_1.JacksonStore, JacksonIndex_1.JacksonIndex, JacksonTTL_1.JacksonTTL],
|
58
58
|
});
|
59
|
+
yield this.dataSource.initialize();
|
59
60
|
break;
|
60
61
|
}
|
61
62
|
catch (err) {
|
@@ -64,9 +65,9 @@ class Sql {
|
|
64
65
|
continue;
|
65
66
|
}
|
66
67
|
}
|
67
|
-
this.storeRepository = this.
|
68
|
-
this.indexRepository = this.
|
69
|
-
this.ttlRepository = this.
|
68
|
+
this.storeRepository = this.dataSource.getRepository(JacksonStore_1.JacksonStore);
|
69
|
+
this.indexRepository = this.dataSource.getRepository(JacksonIndex_1.JacksonIndex);
|
70
|
+
this.ttlRepository = this.dataSource.getRepository(JacksonTTL_1.JacksonTTL);
|
70
71
|
if (this.options.ttl && this.options.cleanupLimit) {
|
71
72
|
this.ttlCleanup = () => __awaiter(this, void 0, void 0, function* () {
|
72
73
|
const now = Date.now();
|
@@ -99,7 +100,7 @@ class Sql {
|
|
99
100
|
}
|
100
101
|
get(namespace, key) {
|
101
102
|
return __awaiter(this, void 0, void 0, function* () {
|
102
|
-
const res = yield this.storeRepository.
|
103
|
+
const res = yield this.storeRepository.findOneBy({
|
103
104
|
key: dbutils.key(namespace, key),
|
104
105
|
});
|
105
106
|
if (res && res.value) {
|
@@ -133,7 +134,7 @@ class Sql {
|
|
133
134
|
}
|
134
135
|
getByIndex(namespace, idx) {
|
135
136
|
return __awaiter(this, void 0, void 0, function* () {
|
136
|
-
const res = yield this.indexRepository.
|
137
|
+
const res = yield this.indexRepository.findBy({
|
137
138
|
key: dbutils.keyForIndex(namespace, idx),
|
138
139
|
});
|
139
140
|
const ret = [];
|
@@ -151,7 +152,7 @@ class Sql {
|
|
151
152
|
}
|
152
153
|
put(namespace, key, val, ttl = 0, ...indexes) {
|
153
154
|
return __awaiter(this, void 0, void 0, function* () {
|
154
|
-
yield this.
|
155
|
+
yield this.dataSource.transaction((transactionalEntityManager) => __awaiter(this, void 0, void 0, function* () {
|
155
156
|
const dbKey = dbutils.key(namespace, key);
|
156
157
|
const store = new JacksonStore_1.JacksonStore();
|
157
158
|
store.key = dbKey;
|
@@ -169,7 +170,7 @@ class Sql {
|
|
169
170
|
// no ttl support for secondary indexes
|
170
171
|
for (const idx of indexes || []) {
|
171
172
|
const key = dbutils.keyForIndex(namespace, idx);
|
172
|
-
const rec = yield this.indexRepository.
|
173
|
+
const rec = yield this.indexRepository.findOneBy({
|
173
174
|
key,
|
174
175
|
storeKey: store.key,
|
175
176
|
});
|
package/dist/db/utils.d.ts
CHANGED
@@ -6,3 +6,5 @@ export declare const keyFromParts: (...parts: string[]) => string;
|
|
6
6
|
export declare const sleep: (ms: number) => Promise<void>;
|
7
7
|
export declare function isNumeric(num: any): boolean;
|
8
8
|
export declare const indexPrefix = "_index";
|
9
|
+
export declare const createdAtPrefix = "_createdAt";
|
10
|
+
export declare const modifiedAtPrefix = "_modifiedAt";
|
package/dist/db/utils.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.indexPrefix = exports.isNumeric = exports.sleep = exports.keyFromParts = exports.keyDigest = exports.keyForIndex = exports.key = void 0;
|
6
|
+
exports.modifiedAtPrefix = exports.createdAtPrefix = exports.indexPrefix = exports.isNumeric = exports.sleep = exports.keyFromParts = exports.keyDigest = exports.keyForIndex = exports.key = void 0;
|
7
7
|
const ripemd160_1 = __importDefault(require("ripemd160"));
|
8
8
|
const key = (namespace, k) => {
|
9
9
|
return namespace + ':' + k;
|
@@ -31,3 +31,5 @@ function isNumeric(num) {
|
|
31
31
|
}
|
32
32
|
exports.isNumeric = isNumeric;
|
33
33
|
exports.indexPrefix = '_index';
|
34
|
+
exports.createdAtPrefix = '_createdAt';
|
35
|
+
exports.modifiedAtPrefix = '_modifiedAt';
|
package/dist/index.d.ts
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
+
import { AdminController } from './controller/admin';
|
1
2
|
import { APIController } from './controller/api';
|
2
3
|
import { OAuthController } from './controller/oauth';
|
3
|
-
import {
|
4
|
+
import { HealthCheckController } from './controller/health-check';
|
5
|
+
import { LogoutController } from './controller/signout';
|
4
6
|
import { JacksonOption } from './typings';
|
5
7
|
export declare const controllers: (opts: JacksonOption) => Promise<{
|
6
8
|
apiController: APIController;
|
7
9
|
oauthController: OAuthController;
|
8
10
|
adminController: AdminController;
|
11
|
+
logoutController: LogoutController;
|
12
|
+
healthCheckController: HealthCheckController;
|
9
13
|
}>;
|
10
14
|
export default controllers;
|
11
15
|
export * from './typings';
|
package/dist/index.js
CHANGED
@@ -27,12 +27,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
27
|
};
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
29
29
|
exports.controllers = void 0;
|
30
|
+
const admin_1 = require("./controller/admin");
|
30
31
|
const api_1 = require("./controller/api");
|
31
32
|
const oauth_1 = require("./controller/oauth");
|
32
|
-
const
|
33
|
+
const health_check_1 = require("./controller/health-check");
|
34
|
+
const signout_1 = require("./controller/signout");
|
33
35
|
const db_1 = __importDefault(require("./db/db"));
|
34
|
-
const read_config_1 = __importDefault(require("./read-config"));
|
35
36
|
const defaultDb_1 = __importDefault(require("./db/defaultDb"));
|
37
|
+
const read_config_1 = __importDefault(require("./read-config"));
|
36
38
|
const defaultOpts = (opts) => {
|
37
39
|
const newOpts = Object.assign({}, opts);
|
38
40
|
if (!newOpts.externalUrl) {
|
@@ -56,8 +58,11 @@ const controllers = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
58
|
const sessionStore = db.store('oauth:session', opts.db.ttl);
|
57
59
|
const codeStore = db.store('oauth:code', opts.db.ttl);
|
58
60
|
const tokenStore = db.store('oauth:token', opts.db.ttl);
|
61
|
+
const healthCheckStore = db.store('_health');
|
59
62
|
const apiController = new api_1.APIController({ configStore });
|
60
63
|
const adminController = new admin_1.AdminController({ configStore });
|
64
|
+
const healthCheckController = new health_check_1.HealthCheckController({ healthCheckStore });
|
65
|
+
yield healthCheckController.init();
|
61
66
|
const oauthController = new oauth_1.OAuthController({
|
62
67
|
configStore,
|
63
68
|
sessionStore,
|
@@ -65,6 +70,11 @@ const controllers = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
70
|
tokenStore,
|
66
71
|
opts,
|
67
72
|
});
|
73
|
+
const logoutController = new signout_1.LogoutController({
|
74
|
+
configStore,
|
75
|
+
sessionStore,
|
76
|
+
opts,
|
77
|
+
});
|
68
78
|
// write pre-loaded config if present
|
69
79
|
if (opts.preLoadedConfig && opts.preLoadedConfig.length > 0) {
|
70
80
|
const configs = yield (0, read_config_1.default)(opts.preLoadedConfig);
|
@@ -79,6 +89,8 @@ const controllers = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
89
|
apiController,
|
80
90
|
oauthController,
|
81
91
|
adminController,
|
92
|
+
logoutController,
|
93
|
+
healthCheckController,
|
82
94
|
};
|
83
95
|
});
|
84
96
|
exports.controllers = controllers;
|
package/dist/saml/saml.d.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { SAMLProfile, SAMLReq } from '../typings';
|
2
2
|
export declare const stripCertHeaderAndFooter: (cert: string) => string;
|
3
|
+
declare function PubKeyInfo(this: any, pubKey: string): void;
|
3
4
|
declare const _default: {
|
4
5
|
request: ({ ssoUrl, entityID, callbackUrl, isPassive, forceAuthn, identifierFormat, providerName, signingKey, publicKey, }: SAMLReq) => {
|
5
6
|
id: string;
|
@@ -8,5 +9,7 @@ declare const _default: {
|
|
8
9
|
parseAsync: (rawAssertion: string) => Promise<SAMLProfile>;
|
9
10
|
validateAsync: (rawAssertion: string, options: any) => Promise<SAMLProfile>;
|
10
11
|
parseMetadataAsync: (idpMeta: string) => Promise<Record<string, any>>;
|
12
|
+
PubKeyInfo: typeof PubKeyInfo;
|
13
|
+
certToPEM: (cert: string) => string;
|
11
14
|
};
|
12
15
|
export default _default;
|
package/dist/saml/saml.js
CHANGED
@@ -37,12 +37,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
38
38
|
exports.stripCertHeaderAndFooter = void 0;
|
39
39
|
const saml20_1 = __importDefault(require("@boxyhq/saml20"));
|
40
|
-
const
|
40
|
+
const crypto_1 = __importDefault(require("crypto"));
|
41
|
+
const rambda = __importStar(require("rambda"));
|
41
42
|
const thumbprint_1 = __importDefault(require("thumbprint"));
|
42
43
|
const xml_crypto_1 = __importDefault(require("xml-crypto"));
|
43
|
-
const
|
44
|
+
const xml2js_1 = __importDefault(require("xml2js"));
|
44
45
|
const xmlbuilder_1 = __importDefault(require("xmlbuilder"));
|
45
|
-
const crypto_1 = __importDefault(require("crypto"));
|
46
46
|
const claims_1 = __importDefault(require("./claims"));
|
47
47
|
const idPrefix = '_';
|
48
48
|
const authnXPath = '/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]';
|
@@ -162,6 +162,8 @@ const parseMetadataAsync = (idpMeta) => __awaiter(void 0, void 0, void 0, functi
|
|
162
162
|
let ssoPostUrl = null;
|
163
163
|
let ssoRedirectUrl = null;
|
164
164
|
let loginType = 'idp';
|
165
|
+
let sloRedirectUrl = null;
|
166
|
+
let sloPostUrl = null;
|
165
167
|
let ssoDes = rambda.pathOr(null, 'EntityDescriptor.IDPSSODescriptor', res);
|
166
168
|
if (!ssoDes) {
|
167
169
|
ssoDes = rambda.pathOr([], 'EntityDescriptor.SPSSODescriptor', res);
|
@@ -187,9 +189,19 @@ const parseMetadataAsync = (idpMeta) => __awaiter(void 0, void 0, void 0, functi
|
|
187
189
|
ssoRedirectUrl = rambda.path('$.Location', ssoSvcRec);
|
188
190
|
}
|
189
191
|
}
|
192
|
+
const sloSvc = ssoDesRec['SingleLogoutService'] || [];
|
193
|
+
for (const sloSvcRec of sloSvc) {
|
194
|
+
if (rambda.pathOr('', '$.Binding', sloSvcRec).endsWith('HTTP-Redirect')) {
|
195
|
+
sloRedirectUrl = rambda.path('$.Location', sloSvcRec);
|
196
|
+
}
|
197
|
+
else if (rambda.pathOr('', '$.Binding', sloSvcRec).endsWith('HTTP-POST')) {
|
198
|
+
sloPostUrl = rambda.path('$.Location', sloSvcRec);
|
199
|
+
}
|
200
|
+
}
|
190
201
|
}
|
191
202
|
const ret = {
|
192
203
|
sso: {},
|
204
|
+
slo: {},
|
193
205
|
};
|
194
206
|
if (entityID) {
|
195
207
|
ret.entityID = entityID;
|
@@ -203,9 +215,26 @@ const parseMetadataAsync = (idpMeta) => __awaiter(void 0, void 0, void 0, functi
|
|
203
215
|
if (ssoRedirectUrl) {
|
204
216
|
ret.sso.redirectUrl = ssoRedirectUrl;
|
205
217
|
}
|
218
|
+
if (sloRedirectUrl) {
|
219
|
+
ret.slo.redirectUrl = sloRedirectUrl;
|
220
|
+
}
|
221
|
+
if (sloPostUrl) {
|
222
|
+
ret.slo.postUrl = sloPostUrl;
|
223
|
+
}
|
206
224
|
ret.loginType = loginType;
|
207
225
|
resolve(ret);
|
208
226
|
});
|
209
227
|
});
|
210
228
|
});
|
211
|
-
|
229
|
+
const certToPEM = (cert) => {
|
230
|
+
if (cert.indexOf('BEGIN CERTIFICATE') === -1 && cert.indexOf('END CERTIFICATE') === -1) {
|
231
|
+
const matches = cert.match(/.{1,64}/g);
|
232
|
+
if (matches) {
|
233
|
+
cert = matches.join('\n');
|
234
|
+
cert = '-----BEGIN CERTIFICATE-----\n' + cert;
|
235
|
+
cert = cert + '\n-----END CERTIFICATE-----\n';
|
236
|
+
}
|
237
|
+
}
|
238
|
+
return cert;
|
239
|
+
};
|
240
|
+
exports.default = { request, parseAsync, validateAsync, parseMetadataAsync, PubKeyInfo, certToPEM };
|
package/dist/typings.d.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
export declare type IdPConfig = {
|
2
2
|
defaultRedirectUrl: string;
|
3
|
-
redirectUrl: string;
|
3
|
+
redirectUrl: string[] | string;
|
4
4
|
tenant: string;
|
5
5
|
product: string;
|
6
6
|
name: string;
|
@@ -37,6 +37,12 @@ export interface IOAuthController {
|
|
37
37
|
export interface IAdminController {
|
38
38
|
getAllConfig(pageOffset?: number, pageLimit?: number): any;
|
39
39
|
}
|
40
|
+
export interface IHealthCheckController {
|
41
|
+
status(): Promise<{
|
42
|
+
status: number;
|
43
|
+
}>;
|
44
|
+
init(): Promise<void>;
|
45
|
+
}
|
40
46
|
export interface OAuthReqBody {
|
41
47
|
response_type: 'code';
|
42
48
|
client_id: string;
|
@@ -131,3 +137,39 @@ export interface JacksonOption {
|
|
131
137
|
db: DatabaseOption;
|
132
138
|
clientSecretVerifier?: string;
|
133
139
|
}
|
140
|
+
export interface SLORequestParams {
|
141
|
+
nameId: string;
|
142
|
+
tenant: string;
|
143
|
+
product: string;
|
144
|
+
redirectUrl?: string;
|
145
|
+
}
|
146
|
+
interface Metadata {
|
147
|
+
sso: {
|
148
|
+
postUrl?: string;
|
149
|
+
redirectUrl: string;
|
150
|
+
};
|
151
|
+
slo: {
|
152
|
+
redirectUrl?: string;
|
153
|
+
postUrl?: string;
|
154
|
+
};
|
155
|
+
entityID: string;
|
156
|
+
thumbprint: string;
|
157
|
+
loginType: 'idp';
|
158
|
+
provider: string;
|
159
|
+
}
|
160
|
+
export interface SAMLConfig {
|
161
|
+
idpMetadata: Metadata;
|
162
|
+
certs: {
|
163
|
+
privateKey: string;
|
164
|
+
publicKey: string;
|
165
|
+
};
|
166
|
+
defaultRedirectUrl: string;
|
167
|
+
}
|
168
|
+
export interface ILogoutController {
|
169
|
+
createRequest(body: SLORequestParams): Promise<{
|
170
|
+
logoutUrl: string | null;
|
171
|
+
logoutForm: string | null;
|
172
|
+
}>;
|
173
|
+
handleResponse(body: SAMLResponsePayload): Promise<any>;
|
174
|
+
}
|
175
|
+
export {};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@boxyhq/saml-jackson",
|
3
|
-
"version": "0.
|
3
|
+
"version": "1.0.1",
|
4
4
|
"description": "SAML Jackson library",
|
5
5
|
"keywords": [
|
6
6
|
"SAML 2.0"
|
@@ -18,12 +18,12 @@
|
|
18
18
|
],
|
19
19
|
"scripts": {
|
20
20
|
"build": "tsc -p tsconfig.build.json",
|
21
|
-
"db:migration:generate:postgres": "ts-node -
|
22
|
-
"db:migration:generate:mysql": "cross-env DB_TYPE=mysql DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node -
|
23
|
-
"db:migration:generate:mariadb": "cross-env DB_TYPE=mariadb DB_URL=mariadb://root@localhost:3306/mysql ts-node -
|
24
|
-
"db:migration:run:postgres": "ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run",
|
25
|
-
"db:migration:run:mysql": "cross-env DB_TYPE=mysql DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run",
|
26
|
-
"db:migration:run:mariadb": "cross-env DB_TYPE=mariadb DB_URL=mariadb://root@localhost:3306/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run",
|
21
|
+
"db:migration:generate:postgres": "ts-node --transpile-only ./node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/postgres/pg_${MIGRATION_NAME}",
|
22
|
+
"db:migration:generate:mysql": "cross-env DB_TYPE=mysql DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/mysql/ms_${MIGRATION_NAME}",
|
23
|
+
"db:migration:generate:mariadb": "cross-env DB_TYPE=mariadb DB_URL=mariadb://root@localhost:3306/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/mariadb/md_${MIGRATION_NAME}",
|
24
|
+
"db:migration:run:postgres": "ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
25
|
+
"db:migration:run:mysql": "cross-env DB_TYPE=mysql DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
26
|
+
"db:migration:run:mariadb": "cross-env DB_TYPE=mariadb DB_URL=mariadb://root@localhost:3306/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
27
27
|
"prepublishOnly": "npm run build",
|
28
28
|
"test": "tap --ts --timeout=100 --coverage test/**/*.test.ts",
|
29
29
|
"sort": "npx sort-package-json"
|
@@ -36,7 +36,7 @@
|
|
36
36
|
"statements": 70
|
37
37
|
},
|
38
38
|
"dependencies": {
|
39
|
-
"@boxyhq/saml20": "0.2.
|
39
|
+
"@boxyhq/saml20": "0.2.1",
|
40
40
|
"@opentelemetry/api-metrics": "0.27.0",
|
41
41
|
"@peculiar/webcrypto": "1.3.2",
|
42
42
|
"@peculiar/x509": "1.6.1",
|
@@ -48,25 +48,25 @@
|
|
48
48
|
"reflect-metadata": "0.1.13",
|
49
49
|
"ripemd160": "2.0.2",
|
50
50
|
"thumbprint": "0.0.1",
|
51
|
-
"typeorm": "0.
|
51
|
+
"typeorm": "0.3.3",
|
52
52
|
"xml-crypto": "2.1.3",
|
53
53
|
"xml2js": "0.4.23",
|
54
54
|
"xmlbuilder": "15.1.1"
|
55
55
|
},
|
56
56
|
"devDependencies": {
|
57
|
-
"@types/node": "17.0.
|
57
|
+
"@types/node": "17.0.23",
|
58
58
|
"@types/sinon": "10.0.11",
|
59
59
|
"@types/tap": "15.0.6",
|
60
|
-
"@typescript-eslint/eslint-plugin": "5.
|
61
|
-
"@typescript-eslint/parser": "5.
|
60
|
+
"@typescript-eslint/eslint-plugin": "5.16.0",
|
61
|
+
"@typescript-eslint/parser": "5.16.0",
|
62
62
|
"cross-env": "7.0.3",
|
63
63
|
"eslint": "8.11.0",
|
64
64
|
"eslint-config-prettier": "8.5.0",
|
65
65
|
"prettier": "2.6.0",
|
66
66
|
"sinon": "13.0.1",
|
67
|
-
"tap": "16.0.
|
67
|
+
"tap": "16.0.1",
|
68
68
|
"ts-node": "10.7.0",
|
69
|
-
"tsconfig-paths": "3.14.
|
69
|
+
"tsconfig-paths": "3.14.1",
|
70
70
|
"typescript": "4.6.2"
|
71
71
|
},
|
72
72
|
"engines": {
|