@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.
- 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 +256 -74
- package/dist/controller/utils.d.ts +4 -1
- package/dist/controller/utils.js +37 -25
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/typings.d.ts +15 -20
- package/package.json +17 -20
- 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,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
|
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(param) {
|
53
65
|
try {
|
54
|
-
const sp = new URLSearchParams(
|
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
|
-
|
106
|
-
if (sp
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
state
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
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(
|
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 =
|
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
|
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;
|
package/dist/controller/utils.js
CHANGED
@@ -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.
|
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
|
-
|
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/
|
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,19 @@ 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;
|
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.
|
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
|
39
|
+
"@boxyhq/saml20": "1.0.2",
|
40
40
|
"@opentelemetry/api-metrics": "0.27.0",
|
41
|
-
"@peculiar/webcrypto": "1.
|
42
|
-
"@peculiar/x509": "1.6.
|
43
|
-
"mongodb": "4.
|
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
|
-
"
|
47
|
-
"redis": "4.0.4",
|
46
|
+
"redis": "4.0.6",
|
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.34",
|
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.25.0",
|
58
|
+
"@typescript-eslint/parser": "5.25.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 };
|