@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.
- package/dist/controller/api.js +3 -3
- package/dist/controller/{signout.d.ts → logout.d.ts} +0 -0
- package/dist/controller/{signout.js → logout.js} +14 -46
- package/dist/controller/oauth/redirect.d.ts +1 -1
- package/dist/controller/oauth/redirect.js +6 -1
- package/dist/controller/oauth.d.ts +5 -3
- package/dist/controller/oauth.js +164 -32
- package/dist/controller/utils.d.ts +1 -1
- package/dist/controller/utils.js +2 -25
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/typings.d.ts +8 -20
- package/package.json +16 -19
- package/dist/saml/saml.d.ts +0 -15
- package/dist/saml/saml.js +0 -240
package/dist/controller/api.js
CHANGED
@@ -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
|
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
|
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
|
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
|
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 =
|
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
|
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
|
-
|
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
|
-
|
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
|
17
|
-
authorize_form
|
17
|
+
redirect_url?: string;
|
18
|
+
authorize_form?: string;
|
18
19
|
}>;
|
19
20
|
samlResponse(body: SAMLResponsePayload): Promise<{
|
20
|
-
redirect_url
|
21
|
+
redirect_url?: string;
|
22
|
+
app_select_form?: string;
|
21
23
|
}>;
|
22
24
|
/**
|
23
25
|
* @swagger
|
package/dist/controller/oauth.js
CHANGED
@@ -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
|
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
|
52
|
-
|
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
|
-
|
104
|
-
if (sp
|
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 =
|
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
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
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 =
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
205
|
-
|
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
|
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 =
|
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
|
5
|
+
export declare const relayStatePrefix = "boxyhq_jackson_";
|
6
6
|
export declare const validateAbsoluteUrl: (url: any, message: any) => void;
|
package/dist/controller/utils.js
CHANGED
@@ -1,36 +1,13 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.validateAbsoluteUrl = exports.
|
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
|
-
|
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/
|
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
|
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
|
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
|
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
|
52
|
-
product
|
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.
|
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
|
39
|
+
"@boxyhq/saml20": "1.0.2",
|
40
40
|
"@opentelemetry/api-metrics": "0.27.0",
|
41
|
-
"@peculiar/webcrypto": "1.3.
|
41
|
+
"@peculiar/webcrypto": "1.3.3",
|
42
42
|
"@peculiar/x509": "1.6.1",
|
43
|
-
"mongodb": "4.
|
43
|
+
"mongodb": "4.5.0",
|
44
44
|
"mysql2": "2.3.3",
|
45
45
|
"pg": "8.7.3",
|
46
|
-
"
|
47
|
-
"redis": "4.0.4",
|
46
|
+
"redis": "4.1.0",
|
48
47
|
"reflect-metadata": "0.1.13",
|
49
48
|
"ripemd160": "2.0.2",
|
50
|
-
"
|
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.
|
54
|
+
"@types/node": "17.0.31",
|
58
55
|
"@types/sinon": "10.0.11",
|
59
|
-
"@types/tap": "15.0.
|
60
|
-
"@typescript-eslint/eslint-plugin": "5.
|
61
|
-
"@typescript-eslint/parser": "5.
|
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.
|
60
|
+
"eslint": "8.15.0",
|
64
61
|
"eslint-config-prettier": "8.5.0",
|
65
|
-
"prettier": "2.6.
|
66
|
-
"sinon": "
|
67
|
-
"tap": "16.0
|
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": "
|
70
|
-
"typescript": "4.6.
|
66
|
+
"tsconfig-paths": "4.0.0",
|
67
|
+
"typescript": "4.6.4"
|
71
68
|
},
|
72
69
|
"engines": {
|
73
70
|
"node": ">=14.18.1 <=16.x"
|
package/dist/saml/saml.d.ts
DELETED
@@ -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 };
|