@boxyhq/saml-jackson 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,17 +41,29 @@ 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(param) {
53
65
  try {
54
- const sp = new URLSearchParams(client_id);
66
+ const sp = new URLSearchParams(param);
55
67
  const tenant = sp.get('tenant');
56
68
  const product = sp.get('product');
57
69
  if (tenant && product) {
@@ -74,20 +86,57 @@ 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, scope, 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');
85
137
  if (!redirect_uri) {
86
138
  throw new error_1.JacksonError('Please specify a redirect URL.', 400);
87
139
  }
88
- if (!state) {
89
- throw new error_1.JacksonError('Please specify a state to safeguard against XSRF attacks.', 400);
90
- }
91
140
  let samlConfig;
92
141
  if (tenant && product) {
93
142
  const samlConfigs = yield this.configStore.getByIndex({
@@ -97,27 +146,71 @@ class OAuthController {
97
146
  if (!samlConfigs || samlConfigs.length === 0) {
98
147
  throw new error_1.JacksonError('SAML configuration not found.', 403);
99
148
  }
100
- // TODO: Support multiple matches
101
149
  samlConfig = samlConfigs[0];
150
+ // Support multiple matches
151
+ const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
152
+ response_type,
153
+ client_id,
154
+ redirect_uri,
155
+ state,
156
+ tenant,
157
+ product,
158
+ code_challenge,
159
+ code_challenge_method,
160
+ provider,
161
+ });
162
+ if (redirect_url) {
163
+ return { redirect_url };
164
+ }
165
+ if (resolvedSamlConfig) {
166
+ samlConfig = resolvedSamlConfig;
167
+ }
102
168
  }
103
169
  else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
104
170
  // if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
105
- const sp = getEncodedClientId(client_id);
106
- if (sp === null || sp === void 0 ? void 0 : sp.tenant) {
171
+ let sp = getEncodedTenantProduct(client_id);
172
+ if (!sp && access_type) {
173
+ sp = getEncodedTenantProduct(access_type);
174
+ }
175
+ if (!sp && scope) {
176
+ sp = getEncodedTenantProduct(scope);
177
+ }
178
+ if (sp && sp.tenant && sp.product) {
107
179
  requestedTenant = sp.tenant;
108
- requestedProduct = sp.product || '';
180
+ requestedProduct = sp.product;
109
181
  const samlConfigs = yield this.configStore.getByIndex({
110
182
  name: utils_1.IndexNames.TenantProduct,
111
- value: dbutils.keyFromParts(sp.tenant, sp.product || ''),
183
+ value: dbutils.keyFromParts(sp.tenant, sp.product),
112
184
  });
113
185
  if (!samlConfigs || samlConfigs.length === 0) {
114
186
  throw new error_1.JacksonError('SAML configuration not found.', 403);
115
187
  }
116
- // TODO: Support multiple matches
117
188
  samlConfig = samlConfigs[0];
189
+ // Support multiple matches
190
+ const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
191
+ response_type,
192
+ client_id,
193
+ redirect_uri,
194
+ state,
195
+ tenant,
196
+ product,
197
+ code_challenge,
198
+ code_challenge_method,
199
+ provider,
200
+ });
201
+ if (redirect_url) {
202
+ return { redirect_url };
203
+ }
204
+ if (resolvedSamlConfig) {
205
+ samlConfig = resolvedSamlConfig;
206
+ }
118
207
  }
119
208
  else {
120
209
  samlConfig = yield this.configStore.get(client_id);
210
+ if (samlConfig) {
211
+ requestedTenant = samlConfig.tenant;
212
+ requestedProduct = samlConfig.product;
213
+ }
121
214
  }
122
215
  }
123
216
  else {
@@ -129,6 +222,24 @@ class OAuthController {
129
222
  if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
130
223
  throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
131
224
  }
225
+ if (!state) {
226
+ return {
227
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
228
+ error: 'invalid_request',
229
+ error_description: 'Please specify a state to safeguard against XSRF attacks',
230
+ redirect_uri,
231
+ }),
232
+ };
233
+ }
234
+ if (response_type !== 'code') {
235
+ return {
236
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
237
+ error: 'unsupported_response_type',
238
+ error_description: 'Only Authorization Code grant is supported',
239
+ redirect_uri,
240
+ }),
241
+ };
242
+ }
132
243
  let ssoUrl;
133
244
  let post = false;
134
245
  const { sso } = samlConfig.idpMetadata;
@@ -141,63 +252,94 @@ class OAuthController {
141
252
  ssoUrl = sso.postUrl;
142
253
  post = true;
143
254
  }
144
- const samlReq = saml_1.default.request({
145
- ssoUrl,
146
- entityID: this.opts.samlAudience,
147
- callbackUrl: this.opts.externalUrl + this.opts.samlPath,
148
- signingKey: samlConfig.certs.privateKey,
149
- publicKey: samlConfig.certs.publicKey,
150
- });
151
- const sessionId = crypto_1.default.randomBytes(16).toString('hex');
152
- const requested = {
153
- tenant: requestedTenant,
154
- product: requestedProduct,
155
- client_id,
156
- state,
157
- };
158
- yield this.sessionStore.put(sessionId, {
159
- id: samlReq.id,
160
- redirect_uri,
161
- response_type,
162
- state,
163
- code_challenge,
164
- code_challenge_method,
165
- requested,
166
- });
167
- const relayState = relayStatePrefix + sessionId;
168
- let redirectUrl;
169
- let authorizeForm;
170
- if (!post) {
171
- // HTTP Redirect binding
172
- redirectUrl = redirect.success(ssoUrl, {
173
- RelayState: relayState,
174
- SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
255
+ else {
256
+ return {
257
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
258
+ error: 'invalid_request',
259
+ error_description: 'SAML binding could not be retrieved',
260
+ redirect_uri,
261
+ }),
262
+ };
263
+ }
264
+ try {
265
+ const samlReq = saml20_1.default.request({
266
+ ssoUrl,
267
+ entityID: this.opts.samlAudience,
268
+ callbackUrl: this.opts.externalUrl + this.opts.samlPath,
269
+ signingKey: samlConfig.certs.privateKey,
270
+ publicKey: samlConfig.certs.publicKey,
271
+ });
272
+ const sessionId = crypto_1.default.randomBytes(16).toString('hex');
273
+ const requested = { client_id, state };
274
+ if (requestedTenant) {
275
+ requested.tenant = requestedTenant;
276
+ }
277
+ if (requestedProduct) {
278
+ requested.product = requestedProduct;
279
+ }
280
+ if (idp_hint) {
281
+ requested.idp_hint = idp_hint;
282
+ }
283
+ yield this.sessionStore.put(sessionId, {
284
+ id: samlReq.id,
285
+ redirect_uri,
286
+ response_type,
287
+ state,
288
+ code_challenge,
289
+ code_challenge_method,
290
+ requested,
175
291
  });
292
+ const relayState = utils_1.relayStatePrefix + sessionId;
293
+ let redirectUrl;
294
+ let authorizeForm;
295
+ if (!post) {
296
+ // HTTP Redirect binding
297
+ redirectUrl = redirect.success(ssoUrl, {
298
+ RelayState: relayState,
299
+ SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
300
+ });
301
+ }
302
+ else {
303
+ // HTTP POST binding
304
+ authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
305
+ {
306
+ name: 'RelayState',
307
+ value: relayState,
308
+ },
309
+ {
310
+ name: 'SAMLRequest',
311
+ value: Buffer.from(samlReq.request).toString('base64'),
312
+ },
313
+ ]);
314
+ }
315
+ return {
316
+ redirect_url: redirectUrl,
317
+ authorize_form: authorizeForm,
318
+ };
176
319
  }
177
- else {
178
- // HTTP POST binding
179
- authorizeForm = (0, utils_1.createRequestForm)(relayState, encodeURI(Buffer.from(samlReq.request).toString('base64')), ssoUrl);
320
+ catch (err) {
321
+ return {
322
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
323
+ error: 'server_error',
324
+ error_description: (0, utils_1.getErrorMessage)(err),
325
+ redirect_uri,
326
+ }),
327
+ };
180
328
  }
181
- return {
182
- redirect_url: redirectUrl,
183
- authorize_form: authorizeForm,
184
- };
185
329
  });
186
330
  }
187
331
  samlResponse(body) {
188
332
  return __awaiter(this, void 0, void 0, function* () {
189
- const { SAMLResponse } = body;
333
+ const { SAMLResponse, idp_hint } = body;
190
334
  let RelayState = body.RelayState || ''; // RelayState will contain the sessionId from earlier quasi-oauth flow
191
- if (!this.opts.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
335
+ const isIdPFlow = !RelayState.startsWith(utils_1.relayStatePrefix);
336
+ if (!this.opts.idpEnabled && isIdPFlow) {
192
337
  // IDP is disabled so block the request
193
338
  throw new error_1.JacksonError('IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.', 403);
194
339
  }
195
- if (!RelayState.startsWith(relayStatePrefix)) {
196
- RelayState = '';
197
- }
198
- RelayState = RelayState.replace(relayStatePrefix, '');
340
+ RelayState = RelayState.replace(utils_1.relayStatePrefix, '');
199
341
  const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
200
- const parsedResp = yield saml_1.default.parseAsync(rawResponse);
342
+ const parsedResp = yield saml20_1.default.parseAsync(rawResponse);
201
343
  const samlConfigs = yield this.configStore.getByIndex({
202
344
  name: utils_1.IndexNames.EntityID,
203
345
  value: parsedResp === null || parsedResp === void 0 ? void 0 : parsedResp.issuer,
@@ -205,6 +347,17 @@ class OAuthController {
205
347
  if (!samlConfigs || samlConfigs.length === 0) {
206
348
  throw new error_1.JacksonError('SAML configuration not found.', 403);
207
349
  }
350
+ let samlConfig = samlConfigs[0];
351
+ if (isIdPFlow) {
352
+ RelayState = '';
353
+ const { resolvedSamlConfig, app_select_form } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, { SAMLResponse }, true);
354
+ if (app_select_form) {
355
+ return { app_select_form };
356
+ }
357
+ if (resolvedSamlConfig) {
358
+ samlConfig = resolvedSamlConfig;
359
+ }
360
+ }
208
361
  let session;
209
362
  if (RelayState !== '') {
210
363
  session = yield this.sessionStore.get(RelayState);
@@ -212,14 +365,17 @@ class OAuthController {
212
365
  throw new error_1.JacksonError('Unable to validate state from the origin request.', 403);
213
366
  }
214
367
  }
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];
368
+ if (!isIdPFlow) {
369
+ // Resolve if there are multiple matches for SP login. TODO: Support multiple matches for IdP login
370
+ samlConfig =
371
+ samlConfigs.length === 1
372
+ ? samlConfigs[0]
373
+ : samlConfigs.filter((c) => {
374
+ var _a, _b, _c;
375
+ return (c.clientID === ((_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.client_id) ||
376
+ (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)));
377
+ })[0];
378
+ }
223
379
  if (!samlConfig) {
224
380
  throw new error_1.JacksonError('SAML configuration not found.', 403);
225
381
  }
@@ -227,10 +383,27 @@ class OAuthController {
227
383
  thumbprint: samlConfig.idpMetadata.thumbprint,
228
384
  audience: this.opts.samlAudience,
229
385
  };
386
+ if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
387
+ throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
388
+ }
230
389
  if (session && session.id) {
231
390
  validateOpts.inResponseTo = session.id;
232
391
  }
233
- const profile = yield saml_1.default.validateAsync(rawResponse, validateOpts);
392
+ let profile;
393
+ const redirect_uri = (session && session.redirect_uri) || samlConfig.defaultRedirectUrl;
394
+ try {
395
+ profile = yield validateResponse(rawResponse, validateOpts);
396
+ }
397
+ catch (err) {
398
+ // return error to redirect_uri
399
+ return {
400
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
401
+ error: 'access_denied',
402
+ error_description: (0, utils_1.getErrorMessage)(err),
403
+ redirect_uri,
404
+ }),
405
+ };
406
+ }
234
407
  // store details against a code
235
408
  const code = crypto_1.default.randomBytes(20).toString('hex');
236
409
  const codeVal = {
@@ -242,9 +415,18 @@ class OAuthController {
242
415
  if (session) {
243
416
  codeVal.session = session;
244
417
  }
245
- yield this.codeStore.put(code, codeVal);
246
- if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
247
- throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
418
+ try {
419
+ yield this.codeStore.put(code, codeVal);
420
+ }
421
+ catch (err) {
422
+ // return error to redirect_uri
423
+ return {
424
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
425
+ error: 'server_error',
426
+ error_description: (0, utils_1.getErrorMessage)(err),
427
+ redirect_uri,
428
+ }),
429
+ };
248
430
  }
249
431
  const params = {
250
432
  code,
@@ -252,7 +434,7 @@ class OAuthController {
252
434
  if (session && session.state) {
253
435
  params.state = session.state;
254
436
  }
255
- const redirectUrl = redirect.success((session && session.redirect_uri) || samlConfig.defaultRedirectUrl, params);
437
+ const redirectUrl = redirect.success(redirect_uri, params);
256
438
  // delete the session
257
439
  try {
258
440
  yield this.sessionStore.delete(RelayState);
@@ -345,7 +527,7 @@ class OAuthController {
345
527
  else if (client_id && client_secret) {
346
528
  // check if we have an encoded client_id
347
529
  if (client_id !== 'dummy') {
348
- const sp = getEncodedClientId(client_id);
530
+ const sp = getEncodedTenantProduct(client_id);
349
531
  if (!sp) {
350
532
  // OAuth flow
351
533
  if (client_id !== codeVal.clientID || client_secret !== codeVal.clientSecret) {
@@ -1,6 +1,9 @@
1
+ import type { OAuthErrorHandlerParams } from '../typings';
1
2
  export declare enum IndexNames {
2
3
  EntityID = "entityID",
3
4
  TenantProduct = "tenantProduct"
4
5
  }
5
- export declare const createRequestForm: (relayState: string, samlReqEnc: string, postUrl: string) => string;
6
+ export declare const relayStatePrefix = "boxyhq_jackson_";
6
7
  export declare const validateAbsoluteUrl: (url: any, message: any) => void;
8
+ export declare const OAuthErrorResponse: ({ error, error_description, redirect_uri }: OAuthErrorHandlerParams) => string;
9
+ export declare function getErrorMessage(error: unknown): string;
@@ -1,36 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateAbsoluteUrl = exports.createRequestForm = exports.IndexNames = void 0;
26
+ exports.getErrorMessage = exports.OAuthErrorResponse = exports.validateAbsoluteUrl = exports.relayStatePrefix = exports.IndexNames = void 0;
4
27
  const error_1 = require("./error");
28
+ const redirect = __importStar(require("./oauth/redirect"));
5
29
  var IndexNames;
6
30
  (function (IndexNames) {
7
31
  IndexNames["EntityID"] = "entityID";
8
32
  IndexNames["TenantProduct"] = "tenantProduct";
9
33
  })(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;
34
+ exports.relayStatePrefix = 'boxyhq_jackson_';
34
35
  const validateAbsoluteUrl = (url, message) => {
35
36
  try {
36
37
  new URL(url);
@@ -40,3 +41,14 @@ const validateAbsoluteUrl = (url, message) => {
40
41
  }
41
42
  };
42
43
  exports.validateAbsoluteUrl = validateAbsoluteUrl;
44
+ const OAuthErrorResponse = ({ error, error_description, redirect_uri }) => {
45
+ return redirect.success(redirect_uri, { error, error_description });
46
+ };
47
+ exports.OAuthErrorResponse = OAuthErrorResponse;
48
+ // https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript
49
+ function getErrorMessage(error) {
50
+ if (error instanceof Error)
51
+ return error.message;
52
+ return String(error);
53
+ }
54
+ exports.getErrorMessage = getErrorMessage;
package/dist/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,19 @@ 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;
55
+ scope?: string;
53
56
  code_challenge: string;
54
57
  code_challenge_method: 'plain' | 'S256' | '';
55
58
  provider: 'saml';
59
+ idp_hint?: string;
56
60
  }
57
61
  export interface SAMLResponsePayload {
58
62
  SAMLResponse: string;
59
63
  RelayState: string;
64
+ idp_hint?: string;
60
65
  }
61
66
  export interface OAuthTokenReq {
62
67
  client_id: string;
@@ -75,6 +80,7 @@ export interface Profile {
75
80
  email: string;
76
81
  firstName: string;
77
82
  lastName: string;
83
+ requested: Record<string, string>;
78
84
  }
79
85
  export interface Index {
80
86
  name: string;
@@ -111,23 +117,6 @@ export interface DatabaseOption {
111
117
  encryptionKey?: string;
112
118
  pageLimit?: number;
113
119
  }
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
120
  export interface JacksonOption {
132
121
  externalUrl: string;
133
122
  samlPath: string;
@@ -136,6 +125,7 @@ export interface JacksonOption {
136
125
  idpEnabled?: boolean;
137
126
  db: DatabaseOption;
138
127
  clientSecretVerifier?: string;
128
+ idpDiscoveryPath?: string;
139
129
  }
140
130
  export interface SLORequestParams {
141
131
  nameId: string;
@@ -172,4 +162,9 @@ export interface ILogoutController {
172
162
  }>;
173
163
  handleResponse(body: SAMLResponsePayload): Promise<any>;
174
164
  }
165
+ export interface OAuthErrorHandlerParams {
166
+ error: 'invalid_request' | 'access_denied' | 'unauthorized_client' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable';
167
+ error_description: string;
168
+ redirect_uri: string;
169
+ }
175
170
  export {};
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.4",
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",
42
- "@peculiar/x509": "1.6.1",
43
- "mongodb": "4.4.1",
41
+ "@peculiar/webcrypto": "1.4.0",
42
+ "@peculiar/x509": "1.6.3",
43
+ "mongodb": "4.6.0",
44
44
  "mysql2": "2.3.3",
45
45
  "pg": "8.7.3",
46
- "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.34",
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.25.0",
58
+ "@typescript-eslint/parser": "5.25.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 };