@boxyhq/saml-jackson 1.0.0 → 1.0.3

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,15 +41,27 @@ 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
- function getEncodedClientId(client_id) {
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
+ });
64
+ function getEncodedTenantProduct(client_id) {
53
65
  try {
54
66
  const sp = new URLSearchParams(client_id);
55
67
  const tenant = sp.get('tenant');
@@ -74,11 +86,53 @@ 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
- const { response_type = 'code', client_id, redirect_uri, state, tenant, product, code_challenge, code_challenge_method = '',
131
+ const { response_type = 'code', client_id, redirect_uri, state, tenant, product, access_type, 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;
134
+ let requestedTenant = tenant;
135
+ let requestedProduct = product;
82
136
  metrics.increment('oauthAuthorize');
83
137
  if (!redirect_uri) {
84
138
  throw new error_1.JacksonError('Please specify a redirect URL.', 400);
@@ -95,25 +149,69 @@ class OAuthController {
95
149
  if (!samlConfigs || samlConfigs.length === 0) {
96
150
  throw new error_1.JacksonError('SAML configuration not found.', 403);
97
151
  }
98
- // TODO: Support multiple matches
99
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
+ }
100
171
  }
101
- else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
172
+ else if ((client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') ||
173
+ (access_type && access_type !== '' && access_type !== 'undefined' && access_type !== 'null')) {
102
174
  // if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
103
- const sp = getEncodedClientId(client_id);
104
- if (sp === null || sp === void 0 ? void 0 : sp.tenant) {
175
+ let sp = getEncodedTenantProduct(client_id);
176
+ if (!sp && access_type) {
177
+ sp = getEncodedTenantProduct(access_type);
178
+ }
179
+ if (sp && sp.tenant && sp.product) {
180
+ requestedTenant = sp.tenant;
181
+ requestedProduct = sp.product;
105
182
  const samlConfigs = yield this.configStore.getByIndex({
106
183
  name: utils_1.IndexNames.TenantProduct,
107
- value: dbutils.keyFromParts(sp.tenant, sp.product || ''),
184
+ value: dbutils.keyFromParts(sp.tenant, sp.product),
108
185
  });
109
186
  if (!samlConfigs || samlConfigs.length === 0) {
110
187
  throw new error_1.JacksonError('SAML configuration not found.', 403);
111
188
  }
112
- // TODO: Support multiple matches
113
189
  samlConfig = samlConfigs[0];
190
+ // Support multiple matches
191
+ const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
192
+ response_type,
193
+ client_id,
194
+ redirect_uri,
195
+ state,
196
+ tenant,
197
+ product,
198
+ code_challenge,
199
+ code_challenge_method,
200
+ provider,
201
+ });
202
+ if (redirect_url) {
203
+ return { redirect_url };
204
+ }
205
+ if (resolvedSamlConfig) {
206
+ samlConfig = resolvedSamlConfig;
207
+ }
114
208
  }
115
209
  else {
116
210
  samlConfig = yield this.configStore.get(client_id);
211
+ if (samlConfig) {
212
+ requestedTenant = samlConfig.tenant;
213
+ requestedProduct = samlConfig.product;
214
+ }
117
215
  }
118
216
  }
119
217
  else {
@@ -137,7 +235,7 @@ class OAuthController {
137
235
  ssoUrl = sso.postUrl;
138
236
  post = true;
139
237
  }
140
- const samlReq = saml_1.default.request({
238
+ const samlReq = saml20_1.default.request({
141
239
  ssoUrl,
142
240
  entityID: this.opts.samlAudience,
143
241
  callbackUrl: this.opts.externalUrl + this.opts.samlPath,
@@ -145,12 +243,16 @@ class OAuthController {
145
243
  publicKey: samlConfig.certs.publicKey,
146
244
  });
147
245
  const sessionId = crypto_1.default.randomBytes(16).toString('hex');
148
- const requestedParams = {
149
- tenant,
150
- product,
151
- client_id,
152
- state,
153
- };
246
+ const requested = { client_id, state };
247
+ if (requestedTenant) {
248
+ requested.tenant = requestedTenant;
249
+ }
250
+ if (requestedProduct) {
251
+ requested.product = requestedProduct;
252
+ }
253
+ if (idp_hint) {
254
+ requested.idp_hint = idp_hint;
255
+ }
154
256
  yield this.sessionStore.put(sessionId, {
155
257
  id: samlReq.id,
156
258
  redirect_uri,
@@ -158,9 +260,9 @@ class OAuthController {
158
260
  state,
159
261
  code_challenge,
160
262
  code_challenge_method,
161
- requested: requestedParams,
263
+ requested,
162
264
  });
163
- const relayState = relayStatePrefix + sessionId;
265
+ const relayState = utils_1.relayStatePrefix + sessionId;
164
266
  let redirectUrl;
165
267
  let authorizeForm;
166
268
  if (!post) {
@@ -172,7 +274,16 @@ class OAuthController {
172
274
  }
173
275
  else {
174
276
  // HTTP POST binding
175
- authorizeForm = (0, utils_1.createRequestForm)(relayState, encodeURI(Buffer.from(samlReq.request).toString('base64')), ssoUrl);
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
+ ]);
176
287
  }
177
288
  return {
178
289
  redirect_url: redirectUrl,
@@ -182,18 +293,16 @@ class OAuthController {
182
293
  }
183
294
  samlResponse(body) {
184
295
  return __awaiter(this, void 0, void 0, function* () {
185
- const { SAMLResponse } = body;
296
+ const { SAMLResponse, idp_hint } = body;
186
297
  let RelayState = body.RelayState || ''; // RelayState will contain the sessionId from earlier quasi-oauth flow
187
- if (!this.opts.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
298
+ const isIdPFlow = !RelayState.startsWith(utils_1.relayStatePrefix);
299
+ if (!this.opts.idpEnabled && isIdPFlow) {
188
300
  // IDP is disabled so block the request
189
301
  throw new error_1.JacksonError('IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.', 403);
190
302
  }
191
- if (!RelayState.startsWith(relayStatePrefix)) {
192
- RelayState = '';
193
- }
194
- RelayState = RelayState.replace(relayStatePrefix, '');
303
+ RelayState = RelayState.replace(utils_1.relayStatePrefix, '');
195
304
  const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
196
- const parsedResp = yield saml_1.default.parseAsync(rawResponse);
305
+ const parsedResp = yield saml20_1.default.parseAsync(rawResponse);
197
306
  const samlConfigs = yield this.configStore.getByIndex({
198
307
  name: utils_1.IndexNames.EntityID,
199
308
  value: parsedResp === null || parsedResp === void 0 ? void 0 : parsedResp.issuer,
@@ -201,8 +310,17 @@ class OAuthController {
201
310
  if (!samlConfigs || samlConfigs.length === 0) {
202
311
  throw new error_1.JacksonError('SAML configuration not found.', 403);
203
312
  }
204
- // TODO: Support multiple matches
205
- const samlConfig = samlConfigs[0];
313
+ let samlConfig = samlConfigs[0];
314
+ if (isIdPFlow) {
315
+ RelayState = '';
316
+ const { resolvedSamlConfig, app_select_form } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, { SAMLResponse }, true);
317
+ if (app_select_form) {
318
+ return { app_select_form };
319
+ }
320
+ if (resolvedSamlConfig) {
321
+ samlConfig = resolvedSamlConfig;
322
+ }
323
+ }
206
324
  let session;
207
325
  if (RelayState !== '') {
208
326
  session = yield this.sessionStore.get(RelayState);
@@ -210,6 +328,20 @@ class OAuthController {
210
328
  throw new error_1.JacksonError('Unable to validate state from the origin request.', 403);
211
329
  }
212
330
  }
331
+ if (!isIdPFlow) {
332
+ // Resolve if there are multiple matches for SP login. TODO: Support multiple matches for IdP login
333
+ samlConfig =
334
+ samlConfigs.length === 1
335
+ ? samlConfigs[0]
336
+ : samlConfigs.filter((c) => {
337
+ var _a, _b, _c;
338
+ return (c.clientID === ((_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.client_id) ||
339
+ (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)));
340
+ })[0];
341
+ }
342
+ if (!samlConfig) {
343
+ throw new error_1.JacksonError('SAML configuration not found.', 403);
344
+ }
213
345
  const validateOpts = {
214
346
  thumbprint: samlConfig.idpMetadata.thumbprint,
215
347
  audience: this.opts.samlAudience,
@@ -217,7 +349,7 @@ class OAuthController {
217
349
  if (session && session.id) {
218
350
  validateOpts.inResponseTo = session.id;
219
351
  }
220
- const profile = yield saml_1.default.validateAsync(rawResponse, validateOpts);
352
+ const profile = yield validateResponse(rawResponse, validateOpts);
221
353
  // store details against a code
222
354
  const code = crypto_1.default.randomBytes(20).toString('hex');
223
355
  const codeVal = {
@@ -332,7 +464,7 @@ class OAuthController {
332
464
  else if (client_id && client_secret) {
333
465
  // check if we have an encoded client_id
334
466
  if (client_id !== 'dummy') {
335
- const sp = getEncodedClientId(client_id);
467
+ const sp = getEncodedTenantProduct(client_id);
336
468
  if (!sp) {
337
469
  // OAuth flow
338
470
  if (client_id !== codeVal.clientID || client_secret !== codeVal.clientSecret) {
@@ -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,18 @@ 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;
54
+ access_type?: string;
53
55
  code_challenge: string;
54
56
  code_challenge_method: 'plain' | 'S256' | '';
55
57
  provider: 'saml';
58
+ idp_hint?: string;
56
59
  }
57
60
  export interface SAMLResponsePayload {
58
61
  SAMLResponse: string;
59
62
  RelayState: string;
63
+ idp_hint?: string;
60
64
  }
61
65
  export interface OAuthTokenReq {
62
66
  client_id: string;
@@ -111,23 +115,6 @@ export interface DatabaseOption {
111
115
  encryptionKey?: string;
112
116
  pageLimit?: number;
113
117
  }
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
118
  export interface JacksonOption {
132
119
  externalUrl: string;
133
120
  samlPath: string;
@@ -136,6 +123,7 @@ export interface JacksonOption {
136
123
  idpEnabled?: boolean;
137
124
  db: DatabaseOption;
138
125
  clientSecretVerifier?: string;
126
+ idpDiscoveryPath?: string;
139
127
  }
140
128
  export interface SLORequestParams {
141
129
  nameId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxyhq/saml-jackson",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
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.1.0",
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.31",
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.23.0",
58
+ "@typescript-eslint/parser": "5.23.0",
62
59
  "cross-env": "7.0.3",
63
- "eslint": "8.11.0",
60
+ "eslint": "8.15.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": "14.0.0",
64
+ "tap": "16.2.0",
68
65
  "ts-node": "10.7.0",
69
- "tsconfig-paths": "3.14.1",
70
- "typescript": "4.6.2"
66
+ "tsconfig-paths": "4.0.0",
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 };