@boxyhq/saml-jackson 1.0.1 → 1.0.2

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.
@@ -50,7 +50,7 @@ exports.APIController = void 0;
50
50
  const crypto_1 = __importDefault(require("crypto"));
51
51
  const dbutils = __importStar(require("../db/utils"));
52
52
  const metrics = __importStar(require("../opentelemetry/metrics"));
53
- const saml_1 = __importDefault(require("../saml/saml"));
53
+ const saml20_1 = __importDefault(require("@boxyhq/saml20"));
54
54
  const x509_1 = __importDefault(require("../saml/x509"));
55
55
  const error_1 = require("./error");
56
56
  const utils_1 = require("./utils");
@@ -187,7 +187,7 @@ class APIController {
187
187
  if (encodedRawMetadata) {
188
188
  metaData = Buffer.from(encodedRawMetadata, 'base64').toString();
189
189
  }
190
- const idpMetadata = yield saml_1.default.parseMetadataAsync(metaData);
190
+ const idpMetadata = yield saml20_1.default.parseMetadataAsync(metaData);
191
191
  // extract provider
192
192
  let providerName = extractHostName(idpMetadata.entityID);
193
193
  if (!providerName) {
@@ -323,7 +323,7 @@ class APIController {
323
323
  }
324
324
  let newMetadata;
325
325
  if (metaData) {
326
- newMetadata = yield saml_1.default.parseMetadataAsync(metaData);
326
+ newMetadata = yield saml20_1.default.parseMetadataAsync(metaData);
327
327
  // extract provider
328
328
  let providerName = extractHostName(newMetadata.entityID);
329
329
  if (!providerName) {
File without changes
@@ -36,21 +36,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.LogoutController = void 0;
39
- const xmldom_1 = require("@xmldom/xmldom");
40
39
  const crypto_1 = __importDefault(require("crypto"));
41
- const thumbprint_1 = __importDefault(require("thumbprint"));
42
40
  const util_1 = require("util");
43
- const xml_crypto_1 = require("xml-crypto");
44
41
  const xml2js_1 = __importDefault(require("xml2js"));
45
42
  const xmlbuilder_1 = __importDefault(require("xmlbuilder"));
46
43
  const zlib_1 = require("zlib");
47
44
  const dbutils = __importStar(require("../db/utils"));
48
- const saml_1 = __importDefault(require("../saml/saml"));
45
+ const saml20_1 = __importDefault(require("@boxyhq/saml20"));
49
46
  const error_1 = require("./error");
50
47
  const redirect = __importStar(require("./oauth/redirect"));
51
48
  const utils_1 = require("./utils");
52
49
  const deflateRawAsync = (0, util_1.promisify)(zlib_1.deflateRaw);
53
50
  const relayStatePrefix = 'boxyhq_jackson_';
51
+ const logoutXPath = "/*[local-name(.)='LogoutRequest']";
54
52
  class LogoutController {
55
53
  constructor({ configStore, sessionStore, opts }) {
56
54
  this.opts = opts;
@@ -97,7 +95,16 @@ class LogoutController {
97
95
  }
98
96
  // HTTP-POST binding
99
97
  if ('postUrl' in slo) {
100
- logoutForm = (0, utils_1.createRequestForm)(relayState, encodeURI(Buffer.from(signedXML).toString('base64')), slo.postUrl);
98
+ logoutForm = saml20_1.default.createPostForm(slo.postUrl, [
99
+ {
100
+ name: 'RelayState',
101
+ value: relayState,
102
+ },
103
+ {
104
+ name: 'SAMLRequest',
105
+ value: Buffer.from(signedXML).toString('base64'),
106
+ },
107
+ ]);
101
108
  }
102
109
  return { logoutUrl, logoutForm };
103
110
  });
@@ -127,7 +134,7 @@ class LogoutController {
127
134
  throw new error_1.JacksonError('SAML configuration not found.', 403);
128
135
  }
129
136
  const { idpMetadata, defaultRedirectUrl } = samlConfigs[0];
130
- if (!(yield hasValidSignature(rawResponse, idpMetadata.thumbprint))) {
137
+ if (!(yield saml20_1.default.validateSignature(rawResponse, null, idpMetadata.thumbprint))) {
131
138
  throw new error_1.JacksonError('Invalid signature.', 403);
132
139
  }
133
140
  try {
@@ -188,44 +195,5 @@ const parseSAMLResponse = (rawResponse) => __awaiter(void 0, void 0, void 0, fun
188
195
  });
189
196
  // Sign the XML
190
197
  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
- });
198
+ return yield saml20_1.default.sign(xml, signingKey, publicKey, logoutXPath);
231
199
  });
@@ -1 +1 @@
1
- export declare const success: (redirectUrl: string, params: Record<string, string>) => string;
1
+ export declare const success: (redirectUrl: string, params: Record<string, string | string[] | undefined>) => string;
@@ -4,7 +4,12 @@ exports.success = void 0;
4
4
  const success = (redirectUrl, params) => {
5
5
  const url = new URL(redirectUrl);
6
6
  for (const [key, value] of Object.entries(params)) {
7
- url.searchParams.set(key, value);
7
+ if (Array.isArray(value)) {
8
+ value.forEach((v) => url.searchParams.append(key, v));
9
+ }
10
+ else if (value !== undefined) {
11
+ url.searchParams.set(key, value);
12
+ }
8
13
  }
9
14
  return url.href;
10
15
  };
@@ -12,12 +12,14 @@ export declare class OAuthController implements IOAuthController {
12
12
  tokenStore: any;
13
13
  opts: any;
14
14
  });
15
+ private resolveMultipleConfigMatches;
15
16
  authorize(body: OAuthReqBody): Promise<{
16
- redirect_url: string;
17
- authorize_form: string;
17
+ redirect_url?: string;
18
+ authorize_form?: string;
18
19
  }>;
19
20
  samlResponse(body: SAMLResponsePayload): Promise<{
20
- redirect_url: string;
21
+ redirect_url?: string;
22
+ app_select_form?: string;
21
23
  }>;
22
24
  /**
23
25
  * @swagger
@@ -41,14 +41,26 @@ const util_1 = require("util");
41
41
  const zlib_1 = require("zlib");
42
42
  const dbutils = __importStar(require("../db/utils"));
43
43
  const metrics = __importStar(require("../opentelemetry/metrics"));
44
- const saml_1 = __importDefault(require("../saml/saml"));
44
+ const saml20_1 = __importDefault(require("@boxyhq/saml20"));
45
+ const claims_1 = __importDefault(require("../saml/claims"));
45
46
  const error_1 = require("./error");
46
47
  const allowed = __importStar(require("./oauth/allowed"));
47
48
  const codeVerifier = __importStar(require("./oauth/code-verifier"));
48
49
  const redirect = __importStar(require("./oauth/redirect"));
49
50
  const utils_1 = require("./utils");
50
51
  const deflateRawAsync = (0, util_1.promisify)(zlib_1.deflateRaw);
51
- const relayStatePrefix = 'boxyhq_jackson_';
52
+ const validateResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0, void 0, function* () {
53
+ const profile = yield saml20_1.default.validateAsync(rawResponse, validateOpts);
54
+ if (profile && profile.claims) {
55
+ // we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
56
+ profile.claims = claims_1.default.map(profile.claims);
57
+ // some providers don't return the id in the assertion, we set it to a sha256 hash of the email
58
+ if (!profile.claims.id && profile.claims.email) {
59
+ profile.claims.id = crypto_1.default.createHash('sha256').update(profile.claims.email).digest('hex');
60
+ }
61
+ }
62
+ return profile;
63
+ });
52
64
  function getEncodedClientId(client_id) {
53
65
  try {
54
66
  const sp = new URLSearchParams(client_id);
@@ -74,11 +86,51 @@ class OAuthController {
74
86
  this.tokenStore = tokenStore;
75
87
  this.opts = opts;
76
88
  }
89
+ resolveMultipleConfigMatches(samlConfigs, idp_hint, originalParams, isIdpFlow = false) {
90
+ if (samlConfigs.length > 1) {
91
+ if (idp_hint) {
92
+ return { resolvedSamlConfig: samlConfigs.find(({ clientID }) => clientID === idp_hint) };
93
+ }
94
+ else if (this.opts.idpDiscoveryPath) {
95
+ if (!isIdpFlow) {
96
+ // redirect to IdP selection page
97
+ const idpList = samlConfigs.map(({ idpMetadata: { provider }, clientID }) => JSON.stringify({
98
+ provider,
99
+ clientID,
100
+ }));
101
+ return {
102
+ redirect_url: redirect.success(this.opts.externalUrl + this.opts.idpDiscoveryPath, Object.assign(Object.assign({}, originalParams), { idp: idpList })),
103
+ };
104
+ }
105
+ else {
106
+ const appList = samlConfigs.map(({ product, name, description, clientID }) => ({
107
+ product,
108
+ name,
109
+ description,
110
+ clientID,
111
+ }));
112
+ return {
113
+ app_select_form: saml20_1.default.createPostForm(this.opts.idpDiscoveryPath, [
114
+ {
115
+ name: 'SAMLResponse',
116
+ value: originalParams.SAMLResponse,
117
+ },
118
+ {
119
+ name: 'app',
120
+ value: encodeURIComponent(JSON.stringify(appList)),
121
+ },
122
+ ]),
123
+ };
124
+ }
125
+ }
126
+ }
127
+ return {};
128
+ }
77
129
  authorize(body) {
78
130
  return __awaiter(this, void 0, void 0, function* () {
79
131
  const { response_type = 'code', client_id, redirect_uri, state, tenant, product, code_challenge, code_challenge_method = '',
80
132
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
81
- provider = 'saml', } = body;
133
+ provider = 'saml', idp_hint, } = body;
82
134
  let requestedTenant = tenant;
83
135
  let requestedProduct = product;
84
136
  metrics.increment('oauthAuthorize');
@@ -97,24 +149,58 @@ class OAuthController {
97
149
  if (!samlConfigs || samlConfigs.length === 0) {
98
150
  throw new error_1.JacksonError('SAML configuration not found.', 403);
99
151
  }
100
- // TODO: Support multiple matches
101
152
  samlConfig = samlConfigs[0];
153
+ // Support multiple matches
154
+ const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
155
+ response_type,
156
+ client_id,
157
+ redirect_uri,
158
+ state,
159
+ tenant,
160
+ product,
161
+ code_challenge,
162
+ code_challenge_method,
163
+ provider,
164
+ });
165
+ if (redirect_url) {
166
+ return { redirect_url };
167
+ }
168
+ if (resolvedSamlConfig) {
169
+ samlConfig = resolvedSamlConfig;
170
+ }
102
171
  }
103
172
  else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
104
173
  // if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
105
174
  const sp = getEncodedClientId(client_id);
106
- if (sp === null || sp === void 0 ? void 0 : sp.tenant) {
175
+ if (sp && sp.tenant && sp.product) {
107
176
  requestedTenant = sp.tenant;
108
- requestedProduct = sp.product || '';
177
+ requestedProduct = sp.product;
109
178
  const samlConfigs = yield this.configStore.getByIndex({
110
179
  name: utils_1.IndexNames.TenantProduct,
111
- value: dbutils.keyFromParts(sp.tenant, sp.product || ''),
180
+ value: dbutils.keyFromParts(sp.tenant, sp.product),
112
181
  });
113
182
  if (!samlConfigs || samlConfigs.length === 0) {
114
183
  throw new error_1.JacksonError('SAML configuration not found.', 403);
115
184
  }
116
- // TODO: Support multiple matches
117
185
  samlConfig = samlConfigs[0];
186
+ // Support multiple matches
187
+ const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
188
+ response_type,
189
+ client_id,
190
+ redirect_uri,
191
+ state,
192
+ tenant,
193
+ product,
194
+ code_challenge,
195
+ code_challenge_method,
196
+ provider,
197
+ });
198
+ if (redirect_url) {
199
+ return { redirect_url };
200
+ }
201
+ if (resolvedSamlConfig) {
202
+ samlConfig = resolvedSamlConfig;
203
+ }
118
204
  }
119
205
  else {
120
206
  samlConfig = yield this.configStore.get(client_id);
@@ -141,7 +227,7 @@ class OAuthController {
141
227
  ssoUrl = sso.postUrl;
142
228
  post = true;
143
229
  }
144
- const samlReq = saml_1.default.request({
230
+ const samlReq = saml20_1.default.request({
145
231
  ssoUrl,
146
232
  entityID: this.opts.samlAudience,
147
233
  callbackUrl: this.opts.externalUrl + this.opts.samlPath,
@@ -149,12 +235,16 @@ class OAuthController {
149
235
  publicKey: samlConfig.certs.publicKey,
150
236
  });
151
237
  const sessionId = crypto_1.default.randomBytes(16).toString('hex');
152
- const requested = {
153
- tenant: requestedTenant,
154
- product: requestedProduct,
155
- client_id,
156
- state,
157
- };
238
+ const requested = { client_id, state };
239
+ if (requestedTenant) {
240
+ requested.tenant = requestedTenant;
241
+ }
242
+ if (requestedProduct) {
243
+ requested.product = requestedProduct;
244
+ }
245
+ if (idp_hint) {
246
+ requested.idp_hint = idp_hint;
247
+ }
158
248
  yield this.sessionStore.put(sessionId, {
159
249
  id: samlReq.id,
160
250
  redirect_uri,
@@ -164,7 +254,7 @@ class OAuthController {
164
254
  code_challenge_method,
165
255
  requested,
166
256
  });
167
- const relayState = relayStatePrefix + sessionId;
257
+ const relayState = utils_1.relayStatePrefix + sessionId;
168
258
  let redirectUrl;
169
259
  let authorizeForm;
170
260
  if (!post) {
@@ -176,7 +266,16 @@ class OAuthController {
176
266
  }
177
267
  else {
178
268
  // HTTP POST binding
179
- authorizeForm = (0, utils_1.createRequestForm)(relayState, encodeURI(Buffer.from(samlReq.request).toString('base64')), ssoUrl);
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
+ ]);
180
279
  }
181
280
  return {
182
281
  redirect_url: redirectUrl,
@@ -186,18 +285,16 @@ class OAuthController {
186
285
  }
187
286
  samlResponse(body) {
188
287
  return __awaiter(this, void 0, void 0, function* () {
189
- const { SAMLResponse } = body;
288
+ const { SAMLResponse, idp_hint } = body;
190
289
  let RelayState = body.RelayState || ''; // RelayState will contain the sessionId from earlier quasi-oauth flow
191
- if (!this.opts.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
290
+ const isIdPFlow = !RelayState.startsWith(utils_1.relayStatePrefix);
291
+ if (!this.opts.idpEnabled && isIdPFlow) {
192
292
  // IDP is disabled so block the request
193
293
  throw new error_1.JacksonError('IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.', 403);
194
294
  }
195
- if (!RelayState.startsWith(relayStatePrefix)) {
196
- RelayState = '';
197
- }
198
- RelayState = RelayState.replace(relayStatePrefix, '');
295
+ RelayState = RelayState.replace(utils_1.relayStatePrefix, '');
199
296
  const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
200
- const parsedResp = yield saml_1.default.parseAsync(rawResponse);
297
+ const parsedResp = yield saml20_1.default.parseAsync(rawResponse);
201
298
  const samlConfigs = yield this.configStore.getByIndex({
202
299
  name: utils_1.IndexNames.EntityID,
203
300
  value: parsedResp === null || parsedResp === void 0 ? void 0 : parsedResp.issuer,
@@ -205,6 +302,17 @@ class OAuthController {
205
302
  if (!samlConfigs || samlConfigs.length === 0) {
206
303
  throw new error_1.JacksonError('SAML configuration not found.', 403);
207
304
  }
305
+ let samlConfig = samlConfigs[0];
306
+ if (isIdPFlow) {
307
+ RelayState = '';
308
+ const { resolvedSamlConfig, app_select_form } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, { SAMLResponse }, true);
309
+ if (app_select_form) {
310
+ return { app_select_form };
311
+ }
312
+ if (resolvedSamlConfig) {
313
+ samlConfig = resolvedSamlConfig;
314
+ }
315
+ }
208
316
  let session;
209
317
  if (RelayState !== '') {
210
318
  session = yield this.sessionStore.get(RelayState);
@@ -212,14 +320,17 @@ class OAuthController {
212
320
  throw new error_1.JacksonError('Unable to validate state from the origin request.', 403);
213
321
  }
214
322
  }
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];
323
+ if (!isIdPFlow) {
324
+ // Resolve if there are multiple matches for SP login. TODO: Support multiple matches for IdP login
325
+ samlConfig =
326
+ samlConfigs.length === 1
327
+ ? samlConfigs[0]
328
+ : samlConfigs.filter((c) => {
329
+ var _a, _b, _c;
330
+ return (c.clientID === ((_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.client_id) ||
331
+ (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)));
332
+ })[0];
333
+ }
223
334
  if (!samlConfig) {
224
335
  throw new error_1.JacksonError('SAML configuration not found.', 403);
225
336
  }
@@ -230,7 +341,7 @@ class OAuthController {
230
341
  if (session && session.id) {
231
342
  validateOpts.inResponseTo = session.id;
232
343
  }
233
- const profile = yield saml_1.default.validateAsync(rawResponse, validateOpts);
344
+ const profile = yield validateResponse(rawResponse, validateOpts);
234
345
  // store details against a code
235
346
  const code = crypto_1.default.randomBytes(20).toString('hex');
236
347
  const codeVal = {
@@ -2,5 +2,5 @@ export declare enum IndexNames {
2
2
  EntityID = "entityID",
3
3
  TenantProduct = "tenantProduct"
4
4
  }
5
- export declare const createRequestForm: (relayState: string, samlReqEnc: string, postUrl: string) => string;
5
+ export declare const relayStatePrefix = "boxyhq_jackson_";
6
6
  export declare const validateAbsoluteUrl: (url: any, message: any) => void;
@@ -1,36 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateAbsoluteUrl = exports.createRequestForm = exports.IndexNames = void 0;
3
+ exports.validateAbsoluteUrl = exports.relayStatePrefix = exports.IndexNames = void 0;
4
4
  const error_1 = require("./error");
5
5
  var IndexNames;
6
6
  (function (IndexNames) {
7
7
  IndexNames["EntityID"] = "entityID";
8
8
  IndexNames["TenantProduct"] = "tenantProduct";
9
9
  })(IndexNames = exports.IndexNames || (exports.IndexNames = {}));
10
- const createRequestForm = (relayState, samlReqEnc, postUrl) => {
11
- const formElements = [
12
- '<!DOCTYPE html>',
13
- '<html>',
14
- '<head>',
15
- '<meta charset="utf-8">',
16
- '<meta http-equiv="x-ua-compatible" content="ie=edge">',
17
- '</head>',
18
- '<body onload="document.forms[0].submit()">',
19
- '<noscript>',
20
- '<p>Note: Since your browser does not support JavaScript, you must press the Continue button once to proceed.</p>',
21
- '</noscript>',
22
- '<form method="post" action="' + encodeURI(postUrl) + '">',
23
- '<input type="hidden" name="RelayState" value="' + relayState + '"/>',
24
- '<input type="hidden" name="SAMLRequest" value="' + samlReqEnc + '"/>',
25
- '<input type="submit" value="Continue" />',
26
- '</form>',
27
- '<script>document.forms[0].style.display="none";</script>',
28
- '</body>',
29
- '</html>',
30
- ];
31
- return formElements.join('');
32
- };
33
- exports.createRequestForm = createRequestForm;
10
+ exports.relayStatePrefix = 'boxyhq_jackson_';
34
11
  const validateAbsoluteUrl = (url, message) => {
35
12
  try {
36
13
  new URL(url);
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { AdminController } from './controller/admin';
2
2
  import { APIController } from './controller/api';
3
3
  import { OAuthController } from './controller/oauth';
4
4
  import { HealthCheckController } from './controller/health-check';
5
- import { LogoutController } from './controller/signout';
5
+ import { LogoutController } from './controller/logout';
6
6
  import { JacksonOption } from './typings';
7
7
  export declare const controllers: (opts: JacksonOption) => Promise<{
8
8
  apiController: APIController;
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ const admin_1 = require("./controller/admin");
31
31
  const api_1 = require("./controller/api");
32
32
  const oauth_1 = require("./controller/oauth");
33
33
  const health_check_1 = require("./controller/health-check");
34
- const signout_1 = require("./controller/signout");
34
+ const logout_1 = require("./controller/logout");
35
35
  const db_1 = __importDefault(require("./db/db"));
36
36
  const defaultDb_1 = __importDefault(require("./db/defaultDb"));
37
37
  const read_config_1 = __importDefault(require("./read-config"));
@@ -58,7 +58,7 @@ const controllers = (opts) => __awaiter(void 0, void 0, void 0, function* () {
58
58
  const sessionStore = db.store('oauth:session', opts.db.ttl);
59
59
  const codeStore = db.store('oauth:code', opts.db.ttl);
60
60
  const tokenStore = db.store('oauth:token', opts.db.ttl);
61
- const healthCheckStore = db.store('_health');
61
+ const healthCheckStore = db.store('_health:check');
62
62
  const apiController = new api_1.APIController({ configStore });
63
63
  const adminController = new admin_1.AdminController({ configStore });
64
64
  const healthCheckController = new health_check_1.HealthCheckController({ healthCheckStore });
@@ -70,7 +70,7 @@ const controllers = (opts) => __awaiter(void 0, void 0, void 0, function* () {
70
70
  tokenStore,
71
71
  opts,
72
72
  });
73
- const logoutController = new signout_1.LogoutController({
73
+ const logoutController = new logout_1.LogoutController({
74
74
  configStore,
75
75
  sessionStore,
76
76
  opts,
package/dist/typings.d.ts CHANGED
@@ -29,7 +29,8 @@ export interface IOAuthController {
29
29
  authorize_form?: string;
30
30
  }>;
31
31
  samlResponse(body: SAMLResponsePayload): Promise<{
32
- redirect_url: string;
32
+ redirect_url?: string;
33
+ app_select_form?: string;
33
34
  }>;
34
35
  token(body: OAuthTokenReq): Promise<OAuthTokenRes>;
35
36
  userInfo(token: string): Promise<Profile>;
@@ -48,15 +49,17 @@ export interface OAuthReqBody {
48
49
  client_id: string;
49
50
  redirect_uri: string;
50
51
  state: string;
51
- tenant: string;
52
- product: string;
52
+ tenant?: string;
53
+ product?: string;
53
54
  code_challenge: string;
54
55
  code_challenge_method: 'plain' | 'S256' | '';
55
56
  provider: 'saml';
57
+ idp_hint?: string;
56
58
  }
57
59
  export interface SAMLResponsePayload {
58
60
  SAMLResponse: string;
59
61
  RelayState: string;
62
+ idp_hint?: string;
60
63
  }
61
64
  export interface OAuthTokenReq {
62
65
  client_id: string;
@@ -111,23 +114,6 @@ export interface DatabaseOption {
111
114
  encryptionKey?: string;
112
115
  pageLimit?: number;
113
116
  }
114
- export interface SAMLReq {
115
- ssoUrl?: string;
116
- entityID: string;
117
- callbackUrl: string;
118
- isPassive?: boolean;
119
- forceAuthn?: boolean;
120
- identifierFormat?: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress';
121
- providerName?: 'BoxyHQ';
122
- signingKey: string;
123
- publicKey: string;
124
- }
125
- export interface SAMLProfile {
126
- audience: string;
127
- claims: Record<string, any>;
128
- issuer: string;
129
- sessionIndex: string;
130
- }
131
117
  export interface JacksonOption {
132
118
  externalUrl: string;
133
119
  samlPath: string;
@@ -136,6 +122,7 @@ export interface JacksonOption {
136
122
  idpEnabled?: boolean;
137
123
  db: DatabaseOption;
138
124
  clientSecretVerifier?: string;
125
+ idpDiscoveryPath?: string;
139
126
  }
140
127
  export interface SLORequestParams {
141
128
  nameId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxyhq/saml-jackson",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "SAML Jackson library",
5
5
  "keywords": [
6
6
  "SAML 2.0"
@@ -36,38 +36,35 @@
36
36
  "statements": 70
37
37
  },
38
38
  "dependencies": {
39
- "@boxyhq/saml20": "0.2.1",
39
+ "@boxyhq/saml20": "1.0.2",
40
40
  "@opentelemetry/api-metrics": "0.27.0",
41
- "@peculiar/webcrypto": "1.3.2",
41
+ "@peculiar/webcrypto": "1.3.3",
42
42
  "@peculiar/x509": "1.6.1",
43
- "mongodb": "4.4.1",
43
+ "mongodb": "4.5.0",
44
44
  "mysql2": "2.3.3",
45
45
  "pg": "8.7.3",
46
- "rambda": "7.0.3",
47
- "redis": "4.0.4",
46
+ "redis": "4.0.6",
48
47
  "reflect-metadata": "0.1.13",
49
48
  "ripemd160": "2.0.2",
50
- "thumbprint": "0.0.1",
51
- "typeorm": "0.3.3",
52
- "xml-crypto": "2.1.3",
49
+ "typeorm": "0.3.6",
53
50
  "xml2js": "0.4.23",
54
51
  "xmlbuilder": "15.1.1"
55
52
  },
56
53
  "devDependencies": {
57
- "@types/node": "17.0.23",
54
+ "@types/node": "17.0.30",
58
55
  "@types/sinon": "10.0.11",
59
- "@types/tap": "15.0.6",
60
- "@typescript-eslint/eslint-plugin": "5.16.0",
61
- "@typescript-eslint/parser": "5.16.0",
56
+ "@types/tap": "15.0.7",
57
+ "@typescript-eslint/eslint-plugin": "5.21.0",
58
+ "@typescript-eslint/parser": "5.21.0",
62
59
  "cross-env": "7.0.3",
63
- "eslint": "8.11.0",
60
+ "eslint": "8.14.0",
64
61
  "eslint-config-prettier": "8.5.0",
65
- "prettier": "2.6.0",
66
- "sinon": "13.0.1",
67
- "tap": "16.0.1",
62
+ "prettier": "2.6.2",
63
+ "sinon": "13.0.2",
64
+ "tap": "16.1.0",
68
65
  "ts-node": "10.7.0",
69
66
  "tsconfig-paths": "3.14.1",
70
- "typescript": "4.6.2"
67
+ "typescript": "4.6.4"
71
68
  },
72
69
  "engines": {
73
70
  "node": ">=14.18.1 <=16.x"
@@ -1,15 +0,0 @@
1
- import { SAMLProfile, SAMLReq } from '../typings';
2
- export declare const stripCertHeaderAndFooter: (cert: string) => string;
3
- declare function PubKeyInfo(this: any, pubKey: string): void;
4
- declare const _default: {
5
- request: ({ ssoUrl, entityID, callbackUrl, isPassive, forceAuthn, identifierFormat, providerName, signingKey, publicKey, }: SAMLReq) => {
6
- id: string;
7
- request: string;
8
- };
9
- parseAsync: (rawAssertion: string) => Promise<SAMLProfile>;
10
- validateAsync: (rawAssertion: string, options: any) => Promise<SAMLProfile>;
11
- parseMetadataAsync: (idpMeta: string) => Promise<Record<string, any>>;
12
- PubKeyInfo: typeof PubKeyInfo;
13
- certToPEM: (cert: string) => string;
14
- };
15
- export default _default;
package/dist/saml/saml.js DELETED
@@ -1,240 +0,0 @@
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.stripCertHeaderAndFooter = void 0;
39
- const saml20_1 = __importDefault(require("@boxyhq/saml20"));
40
- const crypto_1 = __importDefault(require("crypto"));
41
- const rambda = __importStar(require("rambda"));
42
- const thumbprint_1 = __importDefault(require("thumbprint"));
43
- const xml_crypto_1 = __importDefault(require("xml-crypto"));
44
- const xml2js_1 = __importDefault(require("xml2js"));
45
- const xmlbuilder_1 = __importDefault(require("xmlbuilder"));
46
- const claims_1 = __importDefault(require("./claims"));
47
- const idPrefix = '_';
48
- const authnXPath = '/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]';
49
- const issuerXPath = '/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:assertion"]';
50
- const stripCertHeaderAndFooter = (cert) => {
51
- cert = cert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, '');
52
- cert = cert.replace(/-+END CERTIFICATE-+\r?\n?/, '');
53
- cert = cert.replace(/\r\n/g, '\n');
54
- return cert;
55
- };
56
- exports.stripCertHeaderAndFooter = stripCertHeaderAndFooter;
57
- function PubKeyInfo(pubKey) {
58
- this.pubKey = (0, exports.stripCertHeaderAndFooter)(pubKey);
59
- this.getKeyInfo = function (_key, prefix) {
60
- prefix = prefix || '';
61
- prefix = prefix ? prefix + ':' : prefix;
62
- return `<${prefix}X509Data><${prefix}X509Certificate>${this.pubKey}</${prefix}X509Certificate</${prefix}X509Data>`;
63
- };
64
- }
65
- const signRequest = (xml, signingKey, publicKey) => {
66
- if (!xml) {
67
- throw new Error('Please specify xml');
68
- }
69
- if (!signingKey) {
70
- throw new Error('Please specify signingKey');
71
- }
72
- const sig = new xml_crypto_1.default.SignedXml();
73
- sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
74
- sig.keyInfoProvider = new PubKeyInfo(publicKey);
75
- sig.signingKey = signingKey;
76
- sig.addReference(authnXPath, ['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');
77
- sig.computeSignature(xml, {
78
- location: { reference: authnXPath + issuerXPath, action: 'after' },
79
- });
80
- return sig.getSignedXml();
81
- };
82
- const request = ({ ssoUrl, entityID, callbackUrl, isPassive = false, forceAuthn = false, identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', providerName = 'BoxyHQ', signingKey, publicKey, }) => {
83
- const id = idPrefix + crypto_1.default.randomBytes(10).toString('hex');
84
- const date = new Date().toISOString();
85
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
- const samlReq = {
87
- 'samlp:AuthnRequest': {
88
- '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
89
- '@ID': id,
90
- '@Version': '2.0',
91
- '@IssueInstant': date,
92
- '@ProtocolBinding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
93
- '@Destination': ssoUrl,
94
- 'saml:Issuer': {
95
- '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
96
- '#text': entityID,
97
- },
98
- },
99
- };
100
- if (isPassive)
101
- samlReq['samlp:AuthnRequest']['@IsPassive'] = true;
102
- if (forceAuthn) {
103
- samlReq['samlp:AuthnRequest']['@ForceAuthn'] = true;
104
- }
105
- samlReq['samlp:AuthnRequest']['@AssertionConsumerServiceURL'] = callbackUrl;
106
- samlReq['samlp:AuthnRequest']['samlp:NameIDPolicy'] = {
107
- '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
108
- '@Format': identifierFormat,
109
- '@AllowCreate': 'true',
110
- };
111
- if (providerName != null) {
112
- samlReq['samlp:AuthnRequest']['@ProviderName'] = providerName;
113
- }
114
- let xml = xmlbuilder_1.default.create(samlReq).end({});
115
- if (signingKey) {
116
- xml = signRequest(xml, signingKey, publicKey);
117
- }
118
- return {
119
- id,
120
- request: xml,
121
- };
122
- };
123
- const parseAsync = (rawAssertion) => __awaiter(void 0, void 0, void 0, function* () {
124
- return new Promise((resolve, reject) => {
125
- saml20_1.default.parse(rawAssertion, function onParseAsync(err, profile) {
126
- if (err) {
127
- reject(err);
128
- return;
129
- }
130
- resolve(profile);
131
- });
132
- });
133
- });
134
- const validateAsync = (rawAssertion, options) => __awaiter(void 0, void 0, void 0, function* () {
135
- return new Promise((resolve, reject) => {
136
- saml20_1.default.validate(rawAssertion, options, function onValidateAsync(err, profile) {
137
- if (err) {
138
- reject(err);
139
- return;
140
- }
141
- if (profile && profile.claims) {
142
- // we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
143
- profile.claims = claims_1.default.map(profile.claims);
144
- // some providers don't return the id in the assertion, we set it to a sha256 hash of the email
145
- if (!profile.claims.id) {
146
- profile.claims.id = crypto_1.default.createHash('sha256').update(profile.claims.email).digest('hex');
147
- }
148
- }
149
- resolve(profile);
150
- });
151
- });
152
- });
153
- const parseMetadataAsync = (idpMeta) => __awaiter(void 0, void 0, void 0, function* () {
154
- return new Promise((resolve, reject) => {
155
- xml2js_1.default.parseString(idpMeta, { tagNameProcessors: [xml2js_1.default.processors.stripPrefix] }, (err, res) => {
156
- if (err) {
157
- reject(err);
158
- return;
159
- }
160
- const entityID = rambda.path('EntityDescriptor.$.entityID', res);
161
- let X509Certificate = null;
162
- let ssoPostUrl = null;
163
- let ssoRedirectUrl = null;
164
- let loginType = 'idp';
165
- let sloRedirectUrl = null;
166
- let sloPostUrl = null;
167
- let ssoDes = rambda.pathOr(null, 'EntityDescriptor.IDPSSODescriptor', res);
168
- if (!ssoDes) {
169
- ssoDes = rambda.pathOr([], 'EntityDescriptor.SPSSODescriptor', res);
170
- if (!ssoDes) {
171
- loginType = 'sp';
172
- }
173
- }
174
- for (const ssoDesRec of ssoDes) {
175
- const keyDes = ssoDesRec['KeyDescriptor'];
176
- for (const keyDesRec of keyDes) {
177
- if (keyDesRec['$'] && keyDesRec['$'].use === 'signing') {
178
- const ki = keyDesRec['KeyInfo'][0];
179
- const cd = ki['X509Data'][0];
180
- X509Certificate = cd['X509Certificate'][0];
181
- }
182
- }
183
- const ssoSvc = ssoDesRec['SingleSignOnService'] || ssoDesRec['AssertionConsumerService'] || [];
184
- for (const ssoSvcRec of ssoSvc) {
185
- if (rambda.pathOr('', '$.Binding', ssoSvcRec).endsWith('HTTP-POST')) {
186
- ssoPostUrl = rambda.path('$.Location', ssoSvcRec);
187
- }
188
- else if (rambda.pathOr('', '$.Binding', ssoSvcRec).endsWith('HTTP-Redirect')) {
189
- ssoRedirectUrl = rambda.path('$.Location', ssoSvcRec);
190
- }
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
- }
201
- }
202
- const ret = {
203
- sso: {},
204
- slo: {},
205
- };
206
- if (entityID) {
207
- ret.entityID = entityID;
208
- }
209
- if (X509Certificate) {
210
- ret.thumbprint = thumbprint_1.default.calculate(X509Certificate);
211
- }
212
- if (ssoPostUrl) {
213
- ret.sso.postUrl = ssoPostUrl;
214
- }
215
- if (ssoRedirectUrl) {
216
- ret.sso.redirectUrl = ssoRedirectUrl;
217
- }
218
- if (sloRedirectUrl) {
219
- ret.slo.redirectUrl = sloRedirectUrl;
220
- }
221
- if (sloPostUrl) {
222
- ret.slo.postUrl = sloPostUrl;
223
- }
224
- ret.loginType = loginType;
225
- resolve(ret);
226
- });
227
- });
228
- });
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 };