cloudcms-server 3.3.1-beta.8 → 4.0.0-beta.1
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/LICENSE +1 -1
- package/README.md +1 -1
- package/broadcast/broadcast.js +6 -3
- package/broadcast/providers/redis.js +24 -49
- package/clients/nrp.js +117 -0
- package/clients/redis.js +64 -0
- package/d1/index.js +629 -0
- package/d1/index.js.works +203 -0
- package/d1/package.json +86 -0
- package/d1/package.json.works +14 -0
- package/duster/helpers/sample/nyt.js +2 -1
- package/framework/controllers.js +4 -4
- package/index.js +26 -14
- package/insight/insight.js +1 -1
- package/launchpad/index.js +203 -11
- package/launchpad/launchers/cluster.js +103 -110
- package/launchpad/launchers/redis.js +70 -0
- package/launchpad/launchers/single.js +36 -22
- package/locks/locks.js +63 -9
- package/locks/providers/cluster.js +3 -1
- package/locks/providers/memory.js +10 -7
- package/locks/providers/redis.js +62 -82
- package/middleware/admin/admin.js +3 -3
- package/middleware/authentication/adapters/session.js +11 -8
- package/middleware/authentication/authentication.js +28 -16
- package/middleware/authentication/authenticators/default.js +5 -2
- package/middleware/authentication/authenticators/session.js +5 -2
- package/middleware/authentication/providers/saml.js +1 -1
- package/middleware/authorization/authorization.js +11 -8
- package/middleware/awareness/awareness.js +55 -31
- package/middleware/awareness/plugins/editorial.js +4 -4
- package/middleware/awareness/providers/abstract-async.js +107 -84
- package/middleware/awareness/providers/abstract.js +1 -1
- package/middleware/awareness/providers/memory.js +0 -14
- package/middleware/awareness/providers/redis.js +186 -279
- package/middleware/cache/cache.js +4 -2
- package/middleware/cache/providers/redis.js +127 -89
- package/middleware/cache/providers/shared-memory.js +3 -3
- package/middleware/cloudcms/cloudcms.js +22 -16
- package/middleware/form/form.js +3 -3
- package/middleware/modules/modules.js +63 -10
- package/middleware/proxy/proxy.js +8 -21
- package/middleware/stores/stores.js +48 -5
- package/middleware/themes/themes.js +49 -0
- package/middleware/virtual-config/virtual-config.js +11 -8
- package/middleware/wcm/wcm.js +4 -4
- package/notifications/notifications.js +27 -4
- package/package.json +30 -25
- package/server/index.js +508 -412
- package/server/standalone.js +9 -0
- package/temp/clusterlock/index.js +3 -3
- package/temp/clusterlock/package.json +1 -1
- package/temp/passport-saml/LICENSE +23 -0
- package/temp/passport-saml/README.md +406 -0
- package/temp/passport-saml/lib/node-saml/algorithms.d.ts +5 -0
- package/temp/passport-saml/lib/node-saml/algorithms.js +41 -0
- package/temp/passport-saml/lib/node-saml/algorithms.js.map +1 -0
- package/temp/passport-saml/lib/node-saml/index.d.ts +3 -0
- package/temp/passport-saml/lib/node-saml/index.js +6 -0
- package/temp/passport-saml/lib/node-saml/index.js.map +1 -0
- package/temp/passport-saml/lib/node-saml/inmemory-cache-provider.d.ts +45 -0
- package/temp/passport-saml/lib/node-saml/inmemory-cache-provider.js +86 -0
- package/temp/passport-saml/lib/node-saml/inmemory-cache-provider.js.map +1 -0
- package/temp/passport-saml/lib/node-saml/saml-post-signing.d.ts +3 -0
- package/temp/passport-saml/lib/node-saml/saml-post-signing.js +15 -0
- package/temp/passport-saml/lib/node-saml/saml-post-signing.js.map +1 -0
- package/temp/passport-saml/lib/node-saml/saml.d.ts +77 -0
- package/temp/passport-saml/lib/node-saml/saml.js +1170 -0
- package/temp/passport-saml/lib/node-saml/saml.js.map +1 -0
- package/temp/passport-saml/lib/node-saml/types.d.ts +95 -0
- package/temp/passport-saml/lib/node-saml/types.js +8 -0
- package/temp/passport-saml/lib/node-saml/types.js.map +1 -0
- package/temp/passport-saml/lib/node-saml/utility.d.ts +3 -0
- package/temp/passport-saml/lib/node-saml/utility.js +19 -0
- package/temp/passport-saml/lib/node-saml/utility.js.map +1 -0
- package/temp/passport-saml/lib/node-saml/xml.d.ts +21 -0
- package/temp/passport-saml/lib/node-saml/xml.js +140 -0
- package/temp/passport-saml/lib/node-saml/xml.js.map +1 -0
- package/temp/passport-saml/lib/passport-saml/index.d.ts +6 -0
- package/temp/passport-saml/lib/passport-saml/index.js +11 -0
- package/temp/passport-saml/lib/passport-saml/index.js.map +1 -0
- package/temp/passport-saml/lib/passport-saml/multiSamlStrategy.d.ts +13 -0
- package/temp/passport-saml/lib/passport-saml/multiSamlStrategy.js +63 -0
- package/temp/passport-saml/lib/passport-saml/multiSamlStrategy.js.map +1 -0
- package/temp/passport-saml/lib/passport-saml/strategy.d.ts +20 -0
- package/temp/passport-saml/lib/passport-saml/strategy.js +167 -0
- package/temp/passport-saml/lib/passport-saml/strategy.js.map +1 -0
- package/temp/passport-saml/lib/passport-saml/types.d.ts +51 -0
- package/temp/passport-saml/lib/passport-saml/types.js +11 -0
- package/temp/passport-saml/lib/passport-saml/types.js.map +1 -0
- package/temp/passport-saml/package.json +96 -0
- package/util/auth.js +6 -6
- package/util/cloudcms.js +85 -88
- package/util/proxy-factory.js +159 -268
- package/util/redis.js +113 -0
- package/util/renditions.js +12 -6
- package/util/request.js +48 -12
- package/util/util.js +16 -2
- package/launchpad/launchers/sticky-cluster.js +0 -43
- package/temp/memored/.jshintrc +0 -4
- package/temp/memored/README.md +0 -240
- package/temp/memored/demo/demo1.js +0 -37
- package/temp/memored/demo/demo2.js +0 -32
- package/temp/memored/gulpfile.js +0 -8
- package/temp/memored/index.js +0 -343
- package/temp/memored/package.json +0 -54
- package/temp/memored/spec/memored.spec.js +0 -265
- package/web/cms/ice.js +0 -109
- package/web/cms/preview.js +0 -106
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SAML = void 0;
|
|
4
|
+
const debug_1 = require("debug");
|
|
5
|
+
const debug = (0, debug_1.default)("node-saml");
|
|
6
|
+
const zlib = require("zlib");
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const url_1 = require("url");
|
|
9
|
+
const querystring = require("querystring");
|
|
10
|
+
const util = require("util");
|
|
11
|
+
const inmemory_cache_provider_1 = require("./inmemory-cache-provider");
|
|
12
|
+
const algorithms = require("./algorithms");
|
|
13
|
+
const saml_post_signing_1 = require("./saml-post-signing");
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
const types_2 = require("../passport-saml/types");
|
|
16
|
+
const utility_1 = require("./utility");
|
|
17
|
+
const xml_1 = require("./xml");
|
|
18
|
+
const inflateRawAsync = util.promisify(zlib.inflateRaw);
|
|
19
|
+
const deflateRawAsync = util.promisify(zlib.deflateRaw);
|
|
20
|
+
async function processValidlySignedPostRequestAsync(self, doc, dom) {
|
|
21
|
+
const request = doc.LogoutRequest;
|
|
22
|
+
if (request) {
|
|
23
|
+
const profile = {};
|
|
24
|
+
if (request.$.ID) {
|
|
25
|
+
profile.ID = request.$.ID;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
throw new Error("Missing SAML LogoutRequest ID");
|
|
29
|
+
}
|
|
30
|
+
const issuer = request.Issuer;
|
|
31
|
+
if (issuer && issuer[0]._) {
|
|
32
|
+
profile.issuer = issuer[0]._;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw new Error("Missing SAML issuer");
|
|
36
|
+
}
|
|
37
|
+
const nameID = await self._getNameIdAsync(self, dom);
|
|
38
|
+
if (nameID) {
|
|
39
|
+
profile.nameID = nameID.value;
|
|
40
|
+
if (nameID.format) {
|
|
41
|
+
profile.nameIDFormat = nameID.format;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error("Missing SAML NameID");
|
|
46
|
+
}
|
|
47
|
+
const sessionIndex = request.SessionIndex;
|
|
48
|
+
if (sessionIndex) {
|
|
49
|
+
profile.sessionIndex = sessionIndex[0]._;
|
|
50
|
+
}
|
|
51
|
+
return { profile, loggedOut: true };
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new Error("Unknown SAML request message");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function processValidlySignedSamlLogoutAsync(self, doc, dom) {
|
|
58
|
+
const response = doc.LogoutResponse;
|
|
59
|
+
const request = doc.LogoutRequest;
|
|
60
|
+
if (response) {
|
|
61
|
+
return { profile: null, loggedOut: true };
|
|
62
|
+
}
|
|
63
|
+
else if (request) {
|
|
64
|
+
return await processValidlySignedPostRequestAsync(self, doc, dom);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
throw new Error("Unknown SAML response message");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function promiseWithNameID(nameid) {
|
|
71
|
+
const format = xml_1.xpath.selectAttributes(nameid, "@Format");
|
|
72
|
+
return {
|
|
73
|
+
value: nameid.textContent,
|
|
74
|
+
format: format && format[0] && format[0].nodeValue,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
class SAML {
|
|
78
|
+
constructor(ctorOptions) {
|
|
79
|
+
this.options = this.initialize(ctorOptions);
|
|
80
|
+
this.cacheProvider = this.options.cacheProvider;
|
|
81
|
+
}
|
|
82
|
+
initialize(ctorOptions) {
|
|
83
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
84
|
+
if (!ctorOptions) {
|
|
85
|
+
throw new TypeError("SamlOptions required on construction");
|
|
86
|
+
}
|
|
87
|
+
const options = {
|
|
88
|
+
...ctorOptions,
|
|
89
|
+
passive: (_a = ctorOptions.passive) !== null && _a !== void 0 ? _a : false,
|
|
90
|
+
disableRequestedAuthnContext: (_b = ctorOptions.disableRequestedAuthnContext) !== null && _b !== void 0 ? _b : false,
|
|
91
|
+
additionalParams: (_c = ctorOptions.additionalParams) !== null && _c !== void 0 ? _c : {},
|
|
92
|
+
additionalAuthorizeParams: (_d = ctorOptions.additionalAuthorizeParams) !== null && _d !== void 0 ? _d : {},
|
|
93
|
+
additionalLogoutParams: (_e = ctorOptions.additionalLogoutParams) !== null && _e !== void 0 ? _e : {},
|
|
94
|
+
forceAuthn: (_f = ctorOptions.forceAuthn) !== null && _f !== void 0 ? _f : false,
|
|
95
|
+
skipRequestCompression: (_g = ctorOptions.skipRequestCompression) !== null && _g !== void 0 ? _g : false,
|
|
96
|
+
disableRequestAcsUrl: (_h = ctorOptions.disableRequestAcsUrl) !== null && _h !== void 0 ? _h : false,
|
|
97
|
+
acceptedClockSkewMs: (_j = ctorOptions.acceptedClockSkewMs) !== null && _j !== void 0 ? _j : 0,
|
|
98
|
+
maxAssertionAgeMs: (_k = ctorOptions.maxAssertionAgeMs) !== null && _k !== void 0 ? _k : 0,
|
|
99
|
+
path: (_l = ctorOptions.path) !== null && _l !== void 0 ? _l : "/saml/consume",
|
|
100
|
+
host: (_m = ctorOptions.host) !== null && _m !== void 0 ? _m : "localhost",
|
|
101
|
+
issuer: (_o = ctorOptions.issuer) !== null && _o !== void 0 ? _o : "onelogin_saml",
|
|
102
|
+
identifierFormat: ctorOptions.identifierFormat === undefined
|
|
103
|
+
? "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
|
104
|
+
: ctorOptions.identifierFormat,
|
|
105
|
+
wantAssertionsSigned: (_p = ctorOptions.wantAssertionsSigned) !== null && _p !== void 0 ? _p : false,
|
|
106
|
+
authnContext: (_q = ctorOptions.authnContext) !== null && _q !== void 0 ? _q : [
|
|
107
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
|
|
108
|
+
],
|
|
109
|
+
validateInResponseTo: (_r = ctorOptions.validateInResponseTo) !== null && _r !== void 0 ? _r : false,
|
|
110
|
+
//cert: (0, utility_1.assertRequired)(ctorOptions.cert, "cert is required"),
|
|
111
|
+
requestIdExpirationPeriodMs: (_s = ctorOptions.requestIdExpirationPeriodMs) !== null && _s !== void 0 ? _s : 28800000,
|
|
112
|
+
cacheProvider: (_t = ctorOptions.cacheProvider) !== null && _t !== void 0 ? _t : new inmemory_cache_provider_1.CacheProvider({
|
|
113
|
+
keyExpirationPeriodMs: ctorOptions.requestIdExpirationPeriodMs,
|
|
114
|
+
}),
|
|
115
|
+
logoutUrl: (_v = (_u = ctorOptions.logoutUrl) !== null && _u !== void 0 ? _u : ctorOptions.entryPoint) !== null && _v !== void 0 ? _v : "",
|
|
116
|
+
signatureAlgorithm: (_w = ctorOptions.signatureAlgorithm) !== null && _w !== void 0 ? _w : "sha1",
|
|
117
|
+
authnRequestBinding: (_x = ctorOptions.authnRequestBinding) !== null && _x !== void 0 ? _x : "HTTP-Redirect",
|
|
118
|
+
racComparison: (_y = ctorOptions.racComparison) !== null && _y !== void 0 ? _y : "exact",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// CUSTOM
|
|
122
|
+
options.validateAssertion = ctorOptions.validateAssertion;
|
|
123
|
+
if (typeof(options.validateAssertion) === "undefined") {
|
|
124
|
+
options.validateAssertion = true;
|
|
125
|
+
}
|
|
126
|
+
if (options.validateAssertion === false) {
|
|
127
|
+
delete options.cert;
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
{
|
|
131
|
+
options.cert = (0, utility_1.assertRequired)(ctorOptions.cert, "cert is required");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* List of possible values:
|
|
136
|
+
* - exact : Assertion context must exactly match a context in the list
|
|
137
|
+
* - minimum: Assertion context must be at least as strong as a context in the list
|
|
138
|
+
* - maximum: Assertion context must be no stronger than a context in the list
|
|
139
|
+
* - better: Assertion context must be stronger than all contexts in the list
|
|
140
|
+
*/
|
|
141
|
+
if (!["exact", "minimum", "maximum", "better"].includes(options.racComparison)) {
|
|
142
|
+
throw new TypeError("racComparison must be one of ['exact', 'minimum', 'maximum', 'better']");
|
|
143
|
+
}
|
|
144
|
+
return options;
|
|
145
|
+
}
|
|
146
|
+
getCallbackUrl(host) {
|
|
147
|
+
// Post-auth destination
|
|
148
|
+
if (this.options.callbackUrl) {
|
|
149
|
+
return this.options.callbackUrl;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const url = new url_1.URL("http://localhost");
|
|
153
|
+
if (host) {
|
|
154
|
+
url.host = host;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
url.host = this.options.host;
|
|
158
|
+
}
|
|
159
|
+
if (this.options.protocol) {
|
|
160
|
+
url.protocol = this.options.protocol;
|
|
161
|
+
}
|
|
162
|
+
url.pathname = this.options.path;
|
|
163
|
+
return url.toString();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
_generateUniqueID() {
|
|
167
|
+
return crypto.randomBytes(10).toString("hex");
|
|
168
|
+
}
|
|
169
|
+
generateInstant() {
|
|
170
|
+
return new Date().toISOString();
|
|
171
|
+
}
|
|
172
|
+
signRequest(samlMessage) {
|
|
173
|
+
this.options.privateKey = (0, utility_1.assertRequired)(this.options.privateKey, "privateKey is required");
|
|
174
|
+
const samlMessageToSign = {};
|
|
175
|
+
samlMessage.SigAlg = algorithms.getSigningAlgorithm(this.options.signatureAlgorithm);
|
|
176
|
+
const signer = algorithms.getSigner(this.options.signatureAlgorithm);
|
|
177
|
+
if (samlMessage.SAMLRequest) {
|
|
178
|
+
samlMessageToSign.SAMLRequest = samlMessage.SAMLRequest;
|
|
179
|
+
}
|
|
180
|
+
if (samlMessage.SAMLResponse) {
|
|
181
|
+
samlMessageToSign.SAMLResponse = samlMessage.SAMLResponse;
|
|
182
|
+
}
|
|
183
|
+
if (samlMessage.RelayState) {
|
|
184
|
+
samlMessageToSign.RelayState = samlMessage.RelayState;
|
|
185
|
+
}
|
|
186
|
+
if (samlMessage.SigAlg) {
|
|
187
|
+
samlMessageToSign.SigAlg = samlMessage.SigAlg;
|
|
188
|
+
}
|
|
189
|
+
signer.update(querystring.stringify(samlMessageToSign));
|
|
190
|
+
samlMessage.Signature = signer.sign(this._keyToPEM(this.options.privateKey), "base64");
|
|
191
|
+
}
|
|
192
|
+
async generateAuthorizeRequestAsync(isPassive, isHttpPostBinding, host) {
|
|
193
|
+
this.options.entryPoint = (0, utility_1.assertRequired)(this.options.entryPoint, "entryPoint is required");
|
|
194
|
+
const id = "_" + this._generateUniqueID();
|
|
195
|
+
const instant = this.generateInstant();
|
|
196
|
+
if (this.options.validateInResponseTo) {
|
|
197
|
+
await this.cacheProvider.saveAsync(id, instant);
|
|
198
|
+
}
|
|
199
|
+
const request = {
|
|
200
|
+
"samlp:AuthnRequest": {
|
|
201
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
202
|
+
"@ID": id,
|
|
203
|
+
"@Version": "2.0",
|
|
204
|
+
"@IssueInstant": instant,
|
|
205
|
+
"@ProtocolBinding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
206
|
+
"@Destination": this.options.entryPoint,
|
|
207
|
+
"saml:Issuer": {
|
|
208
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
209
|
+
"#text": this.options.issuer,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
if (isPassive)
|
|
214
|
+
request["samlp:AuthnRequest"]["@IsPassive"] = true;
|
|
215
|
+
if (this.options.forceAuthn) {
|
|
216
|
+
request["samlp:AuthnRequest"]["@ForceAuthn"] = true;
|
|
217
|
+
}
|
|
218
|
+
if (!this.options.disableRequestAcsUrl) {
|
|
219
|
+
request["samlp:AuthnRequest"]["@AssertionConsumerServiceURL"] = this.getCallbackUrl(host);
|
|
220
|
+
}
|
|
221
|
+
if (this.options.identifierFormat != null) {
|
|
222
|
+
request["samlp:AuthnRequest"]["samlp:NameIDPolicy"] = {
|
|
223
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
224
|
+
"@Format": this.options.identifierFormat,
|
|
225
|
+
"@AllowCreate": "true",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (!this.options.disableRequestedAuthnContext) {
|
|
229
|
+
const authnContextClassRefs = [];
|
|
230
|
+
this.options.authnContext.forEach(function (value) {
|
|
231
|
+
authnContextClassRefs.push({
|
|
232
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
233
|
+
"#text": value,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
request["samlp:AuthnRequest"]["samlp:RequestedAuthnContext"] = {
|
|
237
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
238
|
+
"@Comparison": this.options.racComparison,
|
|
239
|
+
"saml:AuthnContextClassRef": authnContextClassRefs,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (this.options.attributeConsumingServiceIndex != null) {
|
|
243
|
+
request["samlp:AuthnRequest"]["@AttributeConsumingServiceIndex"] =
|
|
244
|
+
this.options.attributeConsumingServiceIndex;
|
|
245
|
+
}
|
|
246
|
+
if (this.options.providerName != null) {
|
|
247
|
+
request["samlp:AuthnRequest"]["@ProviderName"] = this.options.providerName;
|
|
248
|
+
}
|
|
249
|
+
if (this.options.scoping != null) {
|
|
250
|
+
const scoping = {
|
|
251
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
252
|
+
};
|
|
253
|
+
if (typeof this.options.scoping.proxyCount === "number") {
|
|
254
|
+
scoping["@ProxyCount"] = this.options.scoping.proxyCount;
|
|
255
|
+
}
|
|
256
|
+
if (this.options.scoping.idpList) {
|
|
257
|
+
scoping["samlp:IDPList"] = this.options.scoping.idpList.map((idpListItem) => {
|
|
258
|
+
const formattedIdpListItem = {
|
|
259
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
260
|
+
};
|
|
261
|
+
if (idpListItem.entries) {
|
|
262
|
+
formattedIdpListItem["samlp:IDPEntry"] = idpListItem.entries.map((entry) => {
|
|
263
|
+
const formattedEntry = {
|
|
264
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
265
|
+
};
|
|
266
|
+
formattedEntry["@ProviderID"] = entry.providerId;
|
|
267
|
+
if (entry.name) {
|
|
268
|
+
formattedEntry["@Name"] = entry.name;
|
|
269
|
+
}
|
|
270
|
+
if (entry.loc) {
|
|
271
|
+
formattedEntry["@Loc"] = entry.loc;
|
|
272
|
+
}
|
|
273
|
+
return formattedEntry;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (idpListItem.getComplete) {
|
|
277
|
+
formattedIdpListItem["samlp:GetComplete"] = idpListItem.getComplete;
|
|
278
|
+
}
|
|
279
|
+
return formattedIdpListItem;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (this.options.scoping.requesterId) {
|
|
283
|
+
scoping["samlp:RequesterID"] = this.options.scoping.requesterId;
|
|
284
|
+
}
|
|
285
|
+
request["samlp:AuthnRequest"]["samlp:Scoping"] = scoping;
|
|
286
|
+
}
|
|
287
|
+
let stringRequest = (0, xml_1.buildXmlBuilderObject)(request, false);
|
|
288
|
+
// TODO: maybe we should always sign here
|
|
289
|
+
if (isHttpPostBinding && (0, types_1.isValidSamlSigningOptions)(this.options)) {
|
|
290
|
+
stringRequest = (0, saml_post_signing_1.signAuthnRequestPost)(stringRequest, this.options);
|
|
291
|
+
}
|
|
292
|
+
return stringRequest;
|
|
293
|
+
}
|
|
294
|
+
async _generateLogoutRequest(user) {
|
|
295
|
+
const id = "_" + this._generateUniqueID();
|
|
296
|
+
const instant = this.generateInstant();
|
|
297
|
+
const request = {
|
|
298
|
+
"samlp:LogoutRequest": {
|
|
299
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
300
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
301
|
+
"@ID": id,
|
|
302
|
+
"@Version": "2.0",
|
|
303
|
+
"@IssueInstant": instant,
|
|
304
|
+
"@Destination": this.options.logoutUrl,
|
|
305
|
+
"saml:Issuer": {
|
|
306
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
307
|
+
"#text": this.options.issuer,
|
|
308
|
+
},
|
|
309
|
+
"saml:NameID": {
|
|
310
|
+
"@Format": user.nameIDFormat,
|
|
311
|
+
"#text": user.nameID,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
if (user.nameQualifier != null) {
|
|
316
|
+
request["samlp:LogoutRequest"]["saml:NameID"]["@NameQualifier"] = user.nameQualifier;
|
|
317
|
+
}
|
|
318
|
+
if (user.spNameQualifier != null) {
|
|
319
|
+
request["samlp:LogoutRequest"]["saml:NameID"]["@SPNameQualifier"] = user.spNameQualifier;
|
|
320
|
+
}
|
|
321
|
+
if (user.sessionIndex) {
|
|
322
|
+
request["samlp:LogoutRequest"]["saml2p:SessionIndex"] = {
|
|
323
|
+
"@xmlns:saml2p": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
324
|
+
"#text": user.sessionIndex,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
await this.cacheProvider.saveAsync(id, instant);
|
|
328
|
+
return (0, xml_1.buildXmlBuilderObject)(request, false);
|
|
329
|
+
}
|
|
330
|
+
_generateLogoutResponse(logoutRequest) {
|
|
331
|
+
const id = "_" + this._generateUniqueID();
|
|
332
|
+
const instant = this.generateInstant();
|
|
333
|
+
const request = {
|
|
334
|
+
"samlp:LogoutResponse": {
|
|
335
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
336
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
337
|
+
"@ID": id,
|
|
338
|
+
"@Version": "2.0",
|
|
339
|
+
"@IssueInstant": instant,
|
|
340
|
+
"@Destination": this.options.logoutUrl,
|
|
341
|
+
"@InResponseTo": logoutRequest.ID,
|
|
342
|
+
"saml:Issuer": {
|
|
343
|
+
"#text": this.options.issuer,
|
|
344
|
+
},
|
|
345
|
+
"samlp:Status": {
|
|
346
|
+
"samlp:StatusCode": {
|
|
347
|
+
"@Value": "urn:oasis:names:tc:SAML:2.0:status:Success",
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
return (0, xml_1.buildXmlBuilderObject)(request, false);
|
|
353
|
+
}
|
|
354
|
+
async _requestToUrlAsync(request, response, operation, additionalParameters) {
|
|
355
|
+
this.options.entryPoint = (0, utility_1.assertRequired)(this.options.entryPoint, "entryPoint is required");
|
|
356
|
+
let buffer;
|
|
357
|
+
if (this.options.skipRequestCompression) {
|
|
358
|
+
buffer = Buffer.from((request || response), "utf8");
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
buffer = await deflateRawAsync((request || response));
|
|
362
|
+
}
|
|
363
|
+
const base64 = buffer.toString("base64");
|
|
364
|
+
let target = new url_1.URL(this.options.entryPoint);
|
|
365
|
+
if (operation === "logout") {
|
|
366
|
+
if (this.options.logoutUrl) {
|
|
367
|
+
target = new url_1.URL(this.options.logoutUrl);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else if (operation !== "authorize") {
|
|
371
|
+
throw new Error("Unknown operation: " + operation);
|
|
372
|
+
}
|
|
373
|
+
const samlMessage = request
|
|
374
|
+
? {
|
|
375
|
+
SAMLRequest: base64,
|
|
376
|
+
}
|
|
377
|
+
: {
|
|
378
|
+
SAMLResponse: base64,
|
|
379
|
+
};
|
|
380
|
+
Object.keys(additionalParameters).forEach((k) => {
|
|
381
|
+
samlMessage[k] = additionalParameters[k];
|
|
382
|
+
});
|
|
383
|
+
if (this.options.privateKey != null) {
|
|
384
|
+
if (!this.options.entryPoint) {
|
|
385
|
+
throw new Error('"entryPoint" config parameter is required for signed messages');
|
|
386
|
+
}
|
|
387
|
+
// sets .SigAlg and .Signature
|
|
388
|
+
this.signRequest(samlMessage);
|
|
389
|
+
}
|
|
390
|
+
Object.keys(samlMessage).forEach((k) => {
|
|
391
|
+
target.searchParams.set(k, samlMessage[k]);
|
|
392
|
+
});
|
|
393
|
+
return target.toString();
|
|
394
|
+
}
|
|
395
|
+
_getAdditionalParams(RelayState, operation, overrideParams) {
|
|
396
|
+
const additionalParams = {};
|
|
397
|
+
if (typeof RelayState === "string" && RelayState.length > 0) {
|
|
398
|
+
additionalParams.RelayState = RelayState;
|
|
399
|
+
}
|
|
400
|
+
const optionsAdditionalParams = this.options.additionalParams;
|
|
401
|
+
Object.keys(optionsAdditionalParams).forEach(function (k) {
|
|
402
|
+
additionalParams[k] = optionsAdditionalParams[k];
|
|
403
|
+
});
|
|
404
|
+
let optionsAdditionalParamsForThisOperation = {};
|
|
405
|
+
if (operation == "authorize") {
|
|
406
|
+
optionsAdditionalParamsForThisOperation = this.options.additionalAuthorizeParams;
|
|
407
|
+
}
|
|
408
|
+
if (operation == "logout") {
|
|
409
|
+
optionsAdditionalParamsForThisOperation = this.options.additionalLogoutParams;
|
|
410
|
+
}
|
|
411
|
+
Object.keys(optionsAdditionalParamsForThisOperation).forEach(function (k) {
|
|
412
|
+
additionalParams[k] = optionsAdditionalParamsForThisOperation[k];
|
|
413
|
+
});
|
|
414
|
+
overrideParams = overrideParams !== null && overrideParams !== void 0 ? overrideParams : {};
|
|
415
|
+
Object.keys(overrideParams).forEach(function (k) {
|
|
416
|
+
additionalParams[k] = overrideParams[k];
|
|
417
|
+
});
|
|
418
|
+
return additionalParams;
|
|
419
|
+
}
|
|
420
|
+
async getAuthorizeUrlAsync(RelayState, host, options) {
|
|
421
|
+
const request = await this.generateAuthorizeRequestAsync(this.options.passive, false, host);
|
|
422
|
+
const operation = "authorize";
|
|
423
|
+
const overrideParams = options ? options.additionalParams || {} : {};
|
|
424
|
+
return await this._requestToUrlAsync(request, null, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
425
|
+
}
|
|
426
|
+
async getAuthorizeFormAsync(RelayState, host) {
|
|
427
|
+
this.options.entryPoint = (0, utility_1.assertRequired)(this.options.entryPoint, "entryPoint is required");
|
|
428
|
+
// The quoteattr() function is used in a context, where the result will not be evaluated by javascript
|
|
429
|
+
// but must be interpreted by an XML or HTML parser, and it must absolutely avoid breaking the syntax
|
|
430
|
+
// of an element attribute.
|
|
431
|
+
const quoteattr = function (s, preserveCR) {
|
|
432
|
+
const preserveCRChar = preserveCR ? " " : "\n";
|
|
433
|
+
return (("" + s) // Forces the conversion to string.
|
|
434
|
+
.replace(/&/g, "&") // This MUST be the 1st replacement.
|
|
435
|
+
.replace(/'/g, "'") // The 4 other predefined entities, required.
|
|
436
|
+
.replace(/"/g, """)
|
|
437
|
+
.replace(/</g, "<")
|
|
438
|
+
.replace(/>/g, ">")
|
|
439
|
+
// Add other replacements here for HTML only
|
|
440
|
+
// Or for XML, only if the named entities are defined in its DTD.
|
|
441
|
+
.replace(/\r\n/g, preserveCRChar) // Must be before the next replacement.
|
|
442
|
+
.replace(/[\r\n]/g, preserveCRChar));
|
|
443
|
+
};
|
|
444
|
+
const request = await this.generateAuthorizeRequestAsync(this.options.passive, true, host);
|
|
445
|
+
let buffer;
|
|
446
|
+
if (this.options.skipRequestCompression) {
|
|
447
|
+
buffer = Buffer.from(request, "utf8");
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
buffer = await deflateRawAsync(request);
|
|
451
|
+
}
|
|
452
|
+
const operation = "authorize";
|
|
453
|
+
const additionalParameters = this._getAdditionalParams(RelayState, operation);
|
|
454
|
+
const samlMessage = {
|
|
455
|
+
SAMLRequest: buffer.toString("base64"),
|
|
456
|
+
};
|
|
457
|
+
Object.keys(additionalParameters).forEach((k) => {
|
|
458
|
+
samlMessage[k] = additionalParameters[k] || "";
|
|
459
|
+
});
|
|
460
|
+
const formInputs = Object.keys(samlMessage)
|
|
461
|
+
.map((k) => {
|
|
462
|
+
return '<input type="hidden" name="' + k + '" value="' + quoteattr(samlMessage[k]) + '" />';
|
|
463
|
+
})
|
|
464
|
+
.join("\r\n");
|
|
465
|
+
return [
|
|
466
|
+
"<!DOCTYPE html>",
|
|
467
|
+
"<html>",
|
|
468
|
+
"<head>",
|
|
469
|
+
'<meta charset="utf-8">',
|
|
470
|
+
'<meta http-equiv="x-ua-compatible" content="ie=edge">',
|
|
471
|
+
"</head>",
|
|
472
|
+
'<body onload="document.forms[0].submit()">',
|
|
473
|
+
"<noscript>",
|
|
474
|
+
"<p><strong>Note:</strong> Since your browser does not support JavaScript, you must press the button below once to proceed.</p>",
|
|
475
|
+
"</noscript>",
|
|
476
|
+
'<form method="post" action="' + encodeURI(this.options.entryPoint) + '">',
|
|
477
|
+
formInputs,
|
|
478
|
+
'<input type="submit" value="Submit" />',
|
|
479
|
+
"</form>",
|
|
480
|
+
'<script>document.forms[0].style.display="none";</script>',
|
|
481
|
+
"</body>",
|
|
482
|
+
"</html>",
|
|
483
|
+
].join("\r\n");
|
|
484
|
+
}
|
|
485
|
+
async getLogoutUrlAsync(user, RelayState, options) {
|
|
486
|
+
const request = await this._generateLogoutRequest(user);
|
|
487
|
+
const operation = "logout";
|
|
488
|
+
const overrideParams = options ? options.additionalParams || {} : {};
|
|
489
|
+
return await this._requestToUrlAsync(request, null, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
490
|
+
}
|
|
491
|
+
getLogoutResponseUrl(samlLogoutRequest, RelayState, options, callback) {
|
|
492
|
+
util.callbackify(() => this.getLogoutResponseUrlAsync(samlLogoutRequest, RelayState, options))(callback);
|
|
493
|
+
}
|
|
494
|
+
async getLogoutResponseUrlAsync(samlLogoutRequest, RelayState, options // add RelayState,
|
|
495
|
+
) {
|
|
496
|
+
const response = this._generateLogoutResponse(samlLogoutRequest);
|
|
497
|
+
const operation = "logout";
|
|
498
|
+
const overrideParams = options ? options.additionalParams || {} : {};
|
|
499
|
+
return await this._requestToUrlAsync(null, response, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
500
|
+
}
|
|
501
|
+
_certToPEM(cert) {
|
|
502
|
+
cert = cert.match(/.{1,64}/g).join("\n");
|
|
503
|
+
if (cert.indexOf("-BEGIN CERTIFICATE-") === -1)
|
|
504
|
+
cert = "-----BEGIN CERTIFICATE-----\n" + cert;
|
|
505
|
+
if (cert.indexOf("-END CERTIFICATE-") === -1)
|
|
506
|
+
cert = cert + "\n-----END CERTIFICATE-----\n";
|
|
507
|
+
return cert;
|
|
508
|
+
}
|
|
509
|
+
async certsToCheck() {
|
|
510
|
+
let checkedCerts;
|
|
511
|
+
if (typeof this.options.cert === "function") {
|
|
512
|
+
checkedCerts = await util
|
|
513
|
+
.promisify(this.options.cert)()
|
|
514
|
+
.then((certs) => {
|
|
515
|
+
certs = (0, utility_1.assertRequired)(certs, "callback didn't return cert");
|
|
516
|
+
if (!Array.isArray(certs)) {
|
|
517
|
+
certs = [certs];
|
|
518
|
+
}
|
|
519
|
+
return certs;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
else if (Array.isArray(this.options.cert)) {
|
|
523
|
+
checkedCerts = this.options.cert;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
checkedCerts = [this.options.cert];
|
|
527
|
+
}
|
|
528
|
+
// CUSTOM: don't assert required certs if validateAssertion is false
|
|
529
|
+
if (this.options.validateAssertion == false)
|
|
530
|
+
{
|
|
531
|
+
// skip
|
|
532
|
+
}
|
|
533
|
+
else
|
|
534
|
+
{
|
|
535
|
+
checkedCerts.forEach((cert) => {
|
|
536
|
+
(0, utility_1.assertRequired)(cert, "unknown cert found");
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return checkedCerts;
|
|
540
|
+
}
|
|
541
|
+
// This function checks that the |currentNode| in the |fullXml| document contains exactly 1 valid
|
|
542
|
+
// signature of the |currentNode|.
|
|
543
|
+
//
|
|
544
|
+
// See https://github.com/bergie/passport-saml/issues/19 for references to some of the attack
|
|
545
|
+
// vectors against SAML signature verification.
|
|
546
|
+
validateSignature(fullXml, currentNode, certs) {
|
|
547
|
+
const xpathSigQuery = ".//*[" +
|
|
548
|
+
"local-name(.)='Signature' and " +
|
|
549
|
+
"namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#' and " +
|
|
550
|
+
"descendant::*[local-name(.)='Reference' and @URI='#" +
|
|
551
|
+
currentNode.getAttribute("ID") +
|
|
552
|
+
"']" +
|
|
553
|
+
"]";
|
|
554
|
+
const signatures = xml_1.xpath.selectElements(currentNode, xpathSigQuery);
|
|
555
|
+
// This function is expecting to validate exactly one signature, so if we find more or fewer
|
|
556
|
+
// than that, reject.
|
|
557
|
+
if (signatures.length !== 1) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
const xpathTransformQuery = ".//*[" +
|
|
561
|
+
"local-name(.)='Transform' and " +
|
|
562
|
+
"namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#' and " +
|
|
563
|
+
"ancestor::*[local-name(.)='Reference' and @URI='#" +
|
|
564
|
+
currentNode.getAttribute("ID") +
|
|
565
|
+
"']" +
|
|
566
|
+
"]";
|
|
567
|
+
const transforms = xml_1.xpath.selectElements(currentNode, xpathTransformQuery);
|
|
568
|
+
// Reject also XMLDSIG with more than 2 Transform
|
|
569
|
+
if (transforms.length > 2) {
|
|
570
|
+
// do not return false, throw an error so that it can be caught by tests differently
|
|
571
|
+
throw new Error("Invalid signature, too many transforms");
|
|
572
|
+
}
|
|
573
|
+
const signature = signatures[0];
|
|
574
|
+
return certs.some((certToCheck) => {
|
|
575
|
+
return (0, xml_1.validateXmlSignatureForCert)(signature, this._certToPEM(certToCheck), fullXml, currentNode);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
async validatePostResponseAsync(container, validateAssertion) {
|
|
579
|
+
let xml, doc, inResponseTo;
|
|
580
|
+
try {
|
|
581
|
+
xml = Buffer.from(container.SAMLResponse, "base64").toString("utf8");
|
|
582
|
+
doc = (0, xml_1.parseDomFromString)(xml);
|
|
583
|
+
if (!Object.prototype.hasOwnProperty.call(doc, "documentElement"))
|
|
584
|
+
throw new Error("SAMLResponse is not valid base64-encoded XML");
|
|
585
|
+
const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
|
|
586
|
+
if (inResponseToNodes) {
|
|
587
|
+
inResponseTo = inResponseToNodes.length ? inResponseToNodes[0].nodeValue : null;
|
|
588
|
+
await this.validateInResponseTo(inResponseTo);
|
|
589
|
+
}
|
|
590
|
+
// Check if this document has a valid top-level signature
|
|
591
|
+
var certs = await this.certsToCheck();
|
|
592
|
+
let validSignature = false;
|
|
593
|
+
if (validateAssertion === false)
|
|
594
|
+
{
|
|
595
|
+
validSignature = true;
|
|
596
|
+
}
|
|
597
|
+
else if (this.validateSignature(xml, doc.documentElement, certs)) {
|
|
598
|
+
validSignature = true;
|
|
599
|
+
}
|
|
600
|
+
const assertions = xml_1.xpath.selectElements(doc, "/*[local-name()='Response']/*[local-name()='Assertion']");
|
|
601
|
+
const encryptedAssertions = xml_1.xpath.selectElements(doc, "/*[local-name()='Response']/*[local-name()='EncryptedAssertion']");
|
|
602
|
+
if (assertions.length + encryptedAssertions.length > 1) {
|
|
603
|
+
// There's no reason I know of that we want to handle multiple assertions, and it seems like a
|
|
604
|
+
// potential risk vector for signature scope issues, so treat this as an invalid signature
|
|
605
|
+
throw new Error("Invalid signature: multiple assertions");
|
|
606
|
+
}
|
|
607
|
+
if (assertions.length == 1) {
|
|
608
|
+
if ((this.options.wantAssertionsSigned || !validSignature) &&
|
|
609
|
+
!this.validateSignature(xml, assertions[0], certs)) {
|
|
610
|
+
throw new Error("Invalid signature");
|
|
611
|
+
}
|
|
612
|
+
return await this.processValidlySignedAssertionAsync(assertions[0].toString(), xml, inResponseTo);
|
|
613
|
+
}
|
|
614
|
+
if (encryptedAssertions.length == 1) {
|
|
615
|
+
this.options.decryptionPvk = (0, utility_1.assertRequired)(this.options.decryptionPvk, "No decryption key for encrypted SAML response");
|
|
616
|
+
const encryptedAssertionXml = encryptedAssertions[0].toString();
|
|
617
|
+
const decryptedXml = await (0, xml_1.decryptXml)(encryptedAssertionXml, this.options.decryptionPvk);
|
|
618
|
+
const decryptedDoc = (0, xml_1.parseDomFromString)(decryptedXml);
|
|
619
|
+
const decryptedAssertions = xml_1.xpath.selectElements(decryptedDoc, "/*[local-name()='Assertion']");
|
|
620
|
+
if (decryptedAssertions.length != 1)
|
|
621
|
+
throw new Error("Invalid EncryptedAssertion content");
|
|
622
|
+
if ((this.options.wantAssertionsSigned || !validSignature) &&
|
|
623
|
+
!this.validateSignature(decryptedXml, decryptedAssertions[0], certs)) {
|
|
624
|
+
throw new Error("Invalid signature from encrypted assertion");
|
|
625
|
+
}
|
|
626
|
+
return await this.processValidlySignedAssertionAsync(decryptedAssertions[0].toString(), xml, inResponseTo);
|
|
627
|
+
}
|
|
628
|
+
// If there's no assertion, fall back on xml2js response parsing for the status &
|
|
629
|
+
// LogoutResponse code.
|
|
630
|
+
const xmljsDoc = await (0, xml_1.parseXml2JsFromString)(xml);
|
|
631
|
+
const response = xmljsDoc.Response;
|
|
632
|
+
if (response) {
|
|
633
|
+
const assertion = response.Assertion;
|
|
634
|
+
if (!assertion) {
|
|
635
|
+
const status = response.Status;
|
|
636
|
+
if (status) {
|
|
637
|
+
const statusCode = status[0].StatusCode;
|
|
638
|
+
if (statusCode &&
|
|
639
|
+
statusCode[0].$.Value === "urn:oasis:names:tc:SAML:2.0:status:Responder") {
|
|
640
|
+
const nestedStatusCode = statusCode[0].StatusCode;
|
|
641
|
+
if (nestedStatusCode &&
|
|
642
|
+
nestedStatusCode[0].$.Value === "urn:oasis:names:tc:SAML:2.0:status:NoPassive") {
|
|
643
|
+
if (!validSignature) {
|
|
644
|
+
throw new Error("Invalid signature: NoPassive");
|
|
645
|
+
}
|
|
646
|
+
return { profile: null, loggedOut: false };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// Note that we're not requiring a valid signature before this logic -- since we are
|
|
650
|
+
// throwing an error in any case, and some providers don't sign error results,
|
|
651
|
+
// let's go ahead and give the potentially more helpful error.
|
|
652
|
+
if (statusCode && statusCode[0].$.Value) {
|
|
653
|
+
const msgType = statusCode[0].$.Value.match(/[^:]*$/)[0];
|
|
654
|
+
if (msgType != "Success") {
|
|
655
|
+
let msg = "unspecified";
|
|
656
|
+
if (status[0].StatusMessage) {
|
|
657
|
+
msg = status[0].StatusMessage[0]._;
|
|
658
|
+
}
|
|
659
|
+
else if (statusCode[0].StatusCode) {
|
|
660
|
+
msg = statusCode[0].StatusCode[0].$.Value.match(/[^:]*$/)[0];
|
|
661
|
+
}
|
|
662
|
+
const statusXml = (0, xml_1.buildXml2JsObject)("Status", status[0]);
|
|
663
|
+
throw new types_2.ErrorWithXmlStatus("SAML provider returned " + msgType + " error: " + msg, statusXml);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
throw new Error("Missing SAML assertion");
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
if (!validSignature) {
|
|
672
|
+
throw new Error("Invalid signature: No response found");
|
|
673
|
+
}
|
|
674
|
+
const logoutResponse = xmljsDoc.LogoutResponse;
|
|
675
|
+
if (logoutResponse) {
|
|
676
|
+
return { profile: null, loggedOut: true };
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
throw new Error("Unknown SAML response message");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
debug("validatePostResponse resulted in an error: %s", err);
|
|
685
|
+
if (this.options.validateInResponseTo) {
|
|
686
|
+
await this.cacheProvider.removeAsync(inResponseTo);
|
|
687
|
+
}
|
|
688
|
+
throw err;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async validateInResponseTo(inResponseTo) {
|
|
692
|
+
if (this.options.validateInResponseTo) {
|
|
693
|
+
if (inResponseTo) {
|
|
694
|
+
const result = await this.cacheProvider.getAsync(inResponseTo);
|
|
695
|
+
if (!result)
|
|
696
|
+
throw new Error("InResponseTo is not valid");
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
throw new Error("InResponseTo is missing from response");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async validateRedirectAsync(container, originalQuery, validateAssertion) {
|
|
708
|
+
const samlMessageType = container.SAMLRequest ? "SAMLRequest" : "SAMLResponse";
|
|
709
|
+
const data = Buffer.from(container[samlMessageType], "base64");
|
|
710
|
+
const inflated = await inflateRawAsync(data);
|
|
711
|
+
const dom = (0, xml_1.parseDomFromString)(inflated.toString());
|
|
712
|
+
const doc = await (0, xml_1.parseXml2JsFromString)(inflated);
|
|
713
|
+
samlMessageType === "SAMLResponse"
|
|
714
|
+
? await this.verifyLogoutResponse(doc)
|
|
715
|
+
: this.verifyLogoutRequest(doc);
|
|
716
|
+
await this.hasValidSignatureForRedirect(container, originalQuery);
|
|
717
|
+
return await processValidlySignedSamlLogoutAsync(this, doc, dom);
|
|
718
|
+
}
|
|
719
|
+
async hasValidSignatureForRedirect(container, originalQuery, validateAssertion) {
|
|
720
|
+
const tokens = originalQuery.split("&");
|
|
721
|
+
const getParam = (key) => {
|
|
722
|
+
const exists = tokens.filter((t) => {
|
|
723
|
+
return new RegExp(key).test(t);
|
|
724
|
+
});
|
|
725
|
+
return exists[0];
|
|
726
|
+
};
|
|
727
|
+
if (container.Signature) {
|
|
728
|
+
|
|
729
|
+
// CUSTOM
|
|
730
|
+
if (validateAssertion === false)
|
|
731
|
+
{
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
let urlString = getParam("SAMLRequest") || getParam("SAMLResponse");
|
|
736
|
+
if (getParam("RelayState")) {
|
|
737
|
+
urlString += "&" + getParam("RelayState");
|
|
738
|
+
}
|
|
739
|
+
urlString += "&" + getParam("SigAlg");
|
|
740
|
+
const certs = await this.certsToCheck();
|
|
741
|
+
const hasValidQuerySignature = certs.some((cert) => {
|
|
742
|
+
return this.validateSignatureForRedirect(urlString, container.Signature, container.SigAlg, cert);
|
|
743
|
+
});
|
|
744
|
+
if (!hasValidQuerySignature) {
|
|
745
|
+
throw new Error("Invalid query signature");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
validateSignatureForRedirect(urlString, signature, alg, cert) {
|
|
753
|
+
// See if we support a matching algorithm, case-insensitive. Otherwise, throw error.
|
|
754
|
+
function hasMatch(ourAlgo) {
|
|
755
|
+
// The incoming algorithm is forwarded as a URL.
|
|
756
|
+
// We trim everything before the last # get something we can compare to the Node.js list
|
|
757
|
+
const algFromURI = alg.toLowerCase().replace(/.*#(.*)$/, "$1");
|
|
758
|
+
return ourAlgo.toLowerCase() === algFromURI;
|
|
759
|
+
}
|
|
760
|
+
const i = crypto.getHashes().findIndex(hasMatch);
|
|
761
|
+
let matchingAlgo;
|
|
762
|
+
if (i > -1) {
|
|
763
|
+
matchingAlgo = crypto.getHashes()[i];
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
throw new Error(alg + " is not supported");
|
|
767
|
+
}
|
|
768
|
+
const verifier = crypto.createVerify(matchingAlgo);
|
|
769
|
+
verifier.update(urlString);
|
|
770
|
+
return verifier.verify(this._certToPEM(cert), signature, "base64");
|
|
771
|
+
}
|
|
772
|
+
verifyLogoutRequest(doc) {
|
|
773
|
+
this.verifyIssuer(doc.LogoutRequest);
|
|
774
|
+
const nowMs = new Date().getTime();
|
|
775
|
+
const conditions = doc.LogoutRequest.$;
|
|
776
|
+
const conErr = this.checkTimestampsValidityError(nowMs, conditions.NotBefore, conditions.NotOnOrAfter);
|
|
777
|
+
if (conErr) {
|
|
778
|
+
throw conErr;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async verifyLogoutResponse(doc) {
|
|
782
|
+
const statusCode = doc.LogoutResponse.Status[0].StatusCode[0].$.Value;
|
|
783
|
+
if (statusCode !== "urn:oasis:names:tc:SAML:2.0:status:Success")
|
|
784
|
+
throw new Error("Bad status code: " + statusCode);
|
|
785
|
+
this.verifyIssuer(doc.LogoutResponse);
|
|
786
|
+
const inResponseTo = doc.LogoutResponse.$.InResponseTo;
|
|
787
|
+
if (inResponseTo) {
|
|
788
|
+
return this.validateInResponseTo(inResponseTo);
|
|
789
|
+
}
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
verifyIssuer(samlMessage) {
|
|
793
|
+
if (this.options.idpIssuer != null) {
|
|
794
|
+
const issuer = samlMessage.Issuer;
|
|
795
|
+
if (issuer) {
|
|
796
|
+
if (issuer[0]._ !== this.options.idpIssuer)
|
|
797
|
+
throw new Error("Unknown SAML issuer. Expected: " + this.options.idpIssuer + " Received: " + issuer[0]._);
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
throw new Error("Missing SAML issuer");
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
async processValidlySignedAssertionAsync(xml, samlResponseXml, inResponseTo) {
|
|
805
|
+
let msg;
|
|
806
|
+
const nowMs = new Date().getTime();
|
|
807
|
+
const profile = {};
|
|
808
|
+
const doc = await (0, xml_1.parseXml2JsFromString)(xml);
|
|
809
|
+
const parsedAssertion = doc;
|
|
810
|
+
const assertion = doc.Assertion;
|
|
811
|
+
getInResponseTo: {
|
|
812
|
+
const issuer = assertion.Issuer;
|
|
813
|
+
if (issuer && issuer[0]._) {
|
|
814
|
+
profile.issuer = issuer[0]._;
|
|
815
|
+
}
|
|
816
|
+
if (inResponseTo) {
|
|
817
|
+
profile.inResponseTo = inResponseTo;
|
|
818
|
+
}
|
|
819
|
+
const authnStatement = assertion.AuthnStatement;
|
|
820
|
+
if (authnStatement) {
|
|
821
|
+
if (authnStatement[0].$ && authnStatement[0].$.SessionIndex) {
|
|
822
|
+
profile.sessionIndex = authnStatement[0].$.SessionIndex;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const subject = assertion.Subject;
|
|
826
|
+
let subjectConfirmation, confirmData;
|
|
827
|
+
if (subject) {
|
|
828
|
+
const nameID = subject[0].NameID;
|
|
829
|
+
if (nameID && nameID[0]._) {
|
|
830
|
+
profile.nameID = nameID[0]._;
|
|
831
|
+
if (nameID[0].$ && nameID[0].$.Format) {
|
|
832
|
+
profile.nameIDFormat = nameID[0].$.Format;
|
|
833
|
+
profile.nameQualifier = nameID[0].$.NameQualifier;
|
|
834
|
+
profile.spNameQualifier = nameID[0].$.SPNameQualifier;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
subjectConfirmation = subject[0].SubjectConfirmation
|
|
838
|
+
? subject[0].SubjectConfirmation[0]
|
|
839
|
+
: null;
|
|
840
|
+
confirmData =
|
|
841
|
+
subjectConfirmation && subjectConfirmation.SubjectConfirmationData
|
|
842
|
+
? subjectConfirmation.SubjectConfirmationData[0]
|
|
843
|
+
: null;
|
|
844
|
+
if (subject[0].SubjectConfirmation && subject[0].SubjectConfirmation.length > 1) {
|
|
845
|
+
msg = "Unable to process multiple SubjectConfirmations in SAML assertion";
|
|
846
|
+
throw new Error(msg);
|
|
847
|
+
}
|
|
848
|
+
if (subjectConfirmation) {
|
|
849
|
+
if (confirmData && confirmData.$) {
|
|
850
|
+
const subjectNotBefore = confirmData.$.NotBefore;
|
|
851
|
+
const subjectNotOnOrAfter = confirmData.$.NotOnOrAfter;
|
|
852
|
+
const maxTimeLimitMs = this.processMaxAgeAssertionTime(this.options.maxAssertionAgeMs, subjectNotOnOrAfter, assertion.$.IssueInstant);
|
|
853
|
+
const subjErr = this.checkTimestampsValidityError(nowMs, subjectNotBefore, subjectNotOnOrAfter, maxTimeLimitMs);
|
|
854
|
+
if (subjErr) {
|
|
855
|
+
throw subjErr;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Test to see that if we have a SubjectConfirmation InResponseTo that it matches
|
|
861
|
+
// the 'InResponseTo' attribute set in the Response
|
|
862
|
+
if (this.options.validateInResponseTo) {
|
|
863
|
+
if (subjectConfirmation) {
|
|
864
|
+
if (confirmData && confirmData.$) {
|
|
865
|
+
const subjectInResponseTo = confirmData.$.InResponseTo;
|
|
866
|
+
if (inResponseTo && subjectInResponseTo && subjectInResponseTo != inResponseTo) {
|
|
867
|
+
await this.cacheProvider.removeAsync(inResponseTo);
|
|
868
|
+
throw new Error("InResponseTo is not valid");
|
|
869
|
+
}
|
|
870
|
+
else if (subjectInResponseTo) {
|
|
871
|
+
let foundValidInResponseTo = false;
|
|
872
|
+
const result = await this.cacheProvider.getAsync(subjectInResponseTo);
|
|
873
|
+
if (result) {
|
|
874
|
+
const createdAt = new Date(result);
|
|
875
|
+
if (nowMs < createdAt.getTime() + this.options.requestIdExpirationPeriodMs)
|
|
876
|
+
foundValidInResponseTo = true;
|
|
877
|
+
}
|
|
878
|
+
await this.cacheProvider.removeAsync(inResponseTo);
|
|
879
|
+
if (!foundValidInResponseTo) {
|
|
880
|
+
throw new Error("InResponseTo is not valid");
|
|
881
|
+
}
|
|
882
|
+
break getInResponseTo;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
await this.cacheProvider.removeAsync(inResponseTo);
|
|
888
|
+
break getInResponseTo;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
break getInResponseTo;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const conditions = assertion.Conditions ? assertion.Conditions[0] : null;
|
|
896
|
+
if (assertion.Conditions && assertion.Conditions.length > 1) {
|
|
897
|
+
msg = "Unable to process multiple conditions in SAML assertion";
|
|
898
|
+
throw new Error(msg);
|
|
899
|
+
}
|
|
900
|
+
if (conditions && conditions.$) {
|
|
901
|
+
const maxTimeLimitMs = this.processMaxAgeAssertionTime(this.options.maxAssertionAgeMs, conditions.$.NotOnOrAfter, assertion.$.IssueInstant);
|
|
902
|
+
const conErr = this.checkTimestampsValidityError(nowMs, conditions.$.NotBefore, conditions.$.NotOnOrAfter, maxTimeLimitMs);
|
|
903
|
+
if (conErr)
|
|
904
|
+
throw conErr;
|
|
905
|
+
}
|
|
906
|
+
if (this.options.audience != null) {
|
|
907
|
+
const audienceErr = this.checkAudienceValidityError(this.options.audience, conditions.AudienceRestriction);
|
|
908
|
+
if (audienceErr)
|
|
909
|
+
throw audienceErr;
|
|
910
|
+
}
|
|
911
|
+
const attributeStatement = assertion.AttributeStatement;
|
|
912
|
+
if (attributeStatement) {
|
|
913
|
+
const attributes = [].concat(...attributeStatement
|
|
914
|
+
.filter((attr) => Array.isArray(attr.Attribute))
|
|
915
|
+
.map((attr) => attr.Attribute));
|
|
916
|
+
const attrValueMapper = (value) => {
|
|
917
|
+
const hasChildren = Object.keys(value).some((cur) => {
|
|
918
|
+
return cur !== "_" && cur !== "$";
|
|
919
|
+
});
|
|
920
|
+
return hasChildren ? value : value._;
|
|
921
|
+
};
|
|
922
|
+
if (attributes) {
|
|
923
|
+
const profileAttributes = {};
|
|
924
|
+
attributes.forEach((attribute) => {
|
|
925
|
+
if (!Object.prototype.hasOwnProperty.call(attribute, "AttributeValue")) {
|
|
926
|
+
// if attributes has no AttributeValue child, continue
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const name = attribute.$.Name;
|
|
930
|
+
const value = attribute.AttributeValue.length === 1
|
|
931
|
+
? attrValueMapper(attribute.AttributeValue[0])
|
|
932
|
+
: attribute.AttributeValue.map(attrValueMapper);
|
|
933
|
+
profileAttributes[name] = value;
|
|
934
|
+
// If any property is already present in profile and is also present
|
|
935
|
+
// in attributes, then skip the one from attributes. Handle this
|
|
936
|
+
// conflict gracefully without returning any error
|
|
937
|
+
if (Object.prototype.hasOwnProperty.call(profile, name)) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
profile[name] = value;
|
|
941
|
+
});
|
|
942
|
+
profile.attributes = profileAttributes;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (!profile.mail && profile["urn:oid:0.9.2342.19200300.100.1.3"]) {
|
|
946
|
+
// See https://spaces.internet2.edu/display/InCFederation/Supported+Attribute+Summary
|
|
947
|
+
// for definition of attribute OIDs
|
|
948
|
+
profile.mail = profile["urn:oid:0.9.2342.19200300.100.1.3"];
|
|
949
|
+
}
|
|
950
|
+
if (!profile.email && profile.mail) {
|
|
951
|
+
profile.email = profile.mail;
|
|
952
|
+
}
|
|
953
|
+
profile.getAssertionXml = () => xml.toString();
|
|
954
|
+
profile.getAssertion = () => parsedAssertion;
|
|
955
|
+
profile.getSamlResponseXml = () => samlResponseXml;
|
|
956
|
+
return { profile, loggedOut: false };
|
|
957
|
+
}
|
|
958
|
+
checkTimestampsValidityError(nowMs, notBefore, notOnOrAfter, maxTimeLimitMs) {
|
|
959
|
+
if (this.options.acceptedClockSkewMs == -1)
|
|
960
|
+
return null;
|
|
961
|
+
if (notBefore) {
|
|
962
|
+
const notBeforeMs = this.dateStringToTimestamp(notBefore, "NotBefore");
|
|
963
|
+
if (nowMs + this.options.acceptedClockSkewMs < notBeforeMs)
|
|
964
|
+
return new Error("SAML assertion not yet valid");
|
|
965
|
+
}
|
|
966
|
+
if (notOnOrAfter) {
|
|
967
|
+
const notOnOrAfterMs = this.dateStringToTimestamp(notOnOrAfter, "NotOnOrAfter");
|
|
968
|
+
if (nowMs - this.options.acceptedClockSkewMs >= notOnOrAfterMs)
|
|
969
|
+
return new Error("SAML assertion expired: clocks skewed too much");
|
|
970
|
+
}
|
|
971
|
+
if (maxTimeLimitMs) {
|
|
972
|
+
if (nowMs - this.options.acceptedClockSkewMs >= maxTimeLimitMs)
|
|
973
|
+
return new Error("SAML assertion expired: assertion too old");
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
checkAudienceValidityError(expectedAudience, audienceRestrictions) {
|
|
978
|
+
if (!audienceRestrictions || audienceRestrictions.length < 1) {
|
|
979
|
+
return new Error("SAML assertion has no AudienceRestriction");
|
|
980
|
+
}
|
|
981
|
+
const errors = audienceRestrictions
|
|
982
|
+
.map((restriction) => {
|
|
983
|
+
if (!restriction.Audience || !restriction.Audience[0] || !restriction.Audience[0]._) {
|
|
984
|
+
return new Error("SAML assertion AudienceRestriction has no Audience value");
|
|
985
|
+
}
|
|
986
|
+
if (restriction.Audience[0]._ !== expectedAudience) {
|
|
987
|
+
return new Error("SAML assertion audience mismatch");
|
|
988
|
+
}
|
|
989
|
+
return null;
|
|
990
|
+
})
|
|
991
|
+
.filter((result) => {
|
|
992
|
+
return result !== null;
|
|
993
|
+
});
|
|
994
|
+
if (errors.length > 0) {
|
|
995
|
+
return errors[0];
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
async validatePostRequestAsync(container) {
|
|
1000
|
+
const xml = Buffer.from(container.SAMLRequest, "base64").toString("utf8");
|
|
1001
|
+
const dom = (0, xml_1.parseDomFromString)(xml);
|
|
1002
|
+
const doc = await (0, xml_1.parseXml2JsFromString)(xml);
|
|
1003
|
+
const certs = await this.certsToCheck();
|
|
1004
|
+
if (!this.validateSignature(xml, dom.documentElement, certs)) {
|
|
1005
|
+
throw new Error("Invalid signature on documentElement");
|
|
1006
|
+
}
|
|
1007
|
+
return await processValidlySignedPostRequestAsync(this, doc, dom);
|
|
1008
|
+
}
|
|
1009
|
+
async _getNameIdAsync(self, doc) {
|
|
1010
|
+
const nameIds = xml_1.xpath.selectElements(doc, "/*[local-name()='LogoutRequest']/*[local-name()='NameID']");
|
|
1011
|
+
const encryptedIds = xml_1.xpath.selectElements(doc, "/*[local-name()='LogoutRequest']/*[local-name()='EncryptedID']");
|
|
1012
|
+
if (nameIds.length + encryptedIds.length > 1) {
|
|
1013
|
+
throw new Error("Invalid LogoutRequest");
|
|
1014
|
+
}
|
|
1015
|
+
if (nameIds.length === 1) {
|
|
1016
|
+
return promiseWithNameID(nameIds[0]);
|
|
1017
|
+
}
|
|
1018
|
+
if (encryptedIds.length === 1) {
|
|
1019
|
+
self.options.decryptionPvk = (0, utility_1.assertRequired)(self.options.decryptionPvk, "No decryption key found getting name ID for encrypted SAML response");
|
|
1020
|
+
const encryptedDatas = xml_1.xpath.selectElements(encryptedIds[0], "./*[local-name()='EncryptedData']");
|
|
1021
|
+
if (encryptedDatas.length !== 1) {
|
|
1022
|
+
throw new Error("Invalid LogoutRequest");
|
|
1023
|
+
}
|
|
1024
|
+
const encryptedDataXml = encryptedDatas[0].toString();
|
|
1025
|
+
const decryptedXml = await (0, xml_1.decryptXml)(encryptedDataXml, self.options.decryptionPvk);
|
|
1026
|
+
const decryptedDoc = (0, xml_1.parseDomFromString)(decryptedXml);
|
|
1027
|
+
const decryptedIds = xml_1.xpath.selectElements(decryptedDoc, "/*[local-name()='NameID']");
|
|
1028
|
+
if (decryptedIds.length !== 1) {
|
|
1029
|
+
throw new Error("Invalid EncryptedAssertion content");
|
|
1030
|
+
}
|
|
1031
|
+
return await promiseWithNameID(decryptedIds[0]);
|
|
1032
|
+
}
|
|
1033
|
+
throw new Error("Missing SAML NameID");
|
|
1034
|
+
}
|
|
1035
|
+
generateServiceProviderMetadata(decryptionCert, signingCert) {
|
|
1036
|
+
const metadata = {
|
|
1037
|
+
EntityDescriptor: {
|
|
1038
|
+
"@xmlns": "urn:oasis:names:tc:SAML:2.0:metadata",
|
|
1039
|
+
"@xmlns:ds": "http://www.w3.org/2000/09/xmldsig#",
|
|
1040
|
+
"@entityID": this.options.issuer,
|
|
1041
|
+
"@ID": this.options.issuer.replace(/\W/g, "_"),
|
|
1042
|
+
SPSSODescriptor: {
|
|
1043
|
+
"@protocolSupportEnumeration": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
if (this.options.decryptionPvk != null) {
|
|
1048
|
+
if (!decryptionCert) {
|
|
1049
|
+
throw new Error("Missing decryptionCert while generating metadata for decrypting service provider");
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (this.options.privateKey != null) {
|
|
1053
|
+
if (!signingCert) {
|
|
1054
|
+
throw new Error("Missing signingCert while generating metadata for signing service provider messages");
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (this.options.decryptionPvk != null || this.options.privateKey != null) {
|
|
1058
|
+
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor = [];
|
|
1059
|
+
if (this.options.privateKey != null) {
|
|
1060
|
+
signingCert = signingCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, "");
|
|
1061
|
+
signingCert = signingCert.replace(/-+END CERTIFICATE-+\r?\n?/, "");
|
|
1062
|
+
signingCert = signingCert.replace(/\r\n/g, "\n");
|
|
1063
|
+
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor.push({
|
|
1064
|
+
"@use": "signing",
|
|
1065
|
+
"ds:KeyInfo": {
|
|
1066
|
+
"ds:X509Data": {
|
|
1067
|
+
"ds:X509Certificate": {
|
|
1068
|
+
"#text": signingCert,
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (this.options.decryptionPvk != null) {
|
|
1075
|
+
decryptionCert = decryptionCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, "");
|
|
1076
|
+
decryptionCert = decryptionCert.replace(/-+END CERTIFICATE-+\r?\n?/, "");
|
|
1077
|
+
decryptionCert = decryptionCert.replace(/\r\n/g, "\n");
|
|
1078
|
+
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor.push({
|
|
1079
|
+
"@use": "encryption",
|
|
1080
|
+
"ds:KeyInfo": {
|
|
1081
|
+
"ds:X509Data": {
|
|
1082
|
+
"ds:X509Certificate": {
|
|
1083
|
+
"#text": decryptionCert,
|
|
1084
|
+
},
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
EncryptionMethod: [
|
|
1088
|
+
// this should be the set that the xmlenc library supports
|
|
1089
|
+
{ "@Algorithm": "http://www.w3.org/2009/xmlenc11#aes256-gcm" },
|
|
1090
|
+
{ "@Algorithm": "http://www.w3.org/2009/xmlenc11#aes128-gcm" },
|
|
1091
|
+
{ "@Algorithm": "http://www.w3.org/2001/04/xmlenc#aes256-cbc" },
|
|
1092
|
+
{ "@Algorithm": "http://www.w3.org/2001/04/xmlenc#aes128-cbc" },
|
|
1093
|
+
],
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (this.options.logoutCallbackUrl != null) {
|
|
1098
|
+
metadata.EntityDescriptor.SPSSODescriptor.SingleLogoutService = {
|
|
1099
|
+
"@Binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
1100
|
+
"@Location": this.options.logoutCallbackUrl,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
if (this.options.identifierFormat != null) {
|
|
1104
|
+
metadata.EntityDescriptor.SPSSODescriptor.NameIDFormat = this.options.identifierFormat;
|
|
1105
|
+
}
|
|
1106
|
+
if (this.options.wantAssertionsSigned) {
|
|
1107
|
+
metadata.EntityDescriptor.SPSSODescriptor["@WantAssertionsSigned"] = true;
|
|
1108
|
+
}
|
|
1109
|
+
metadata.EntityDescriptor.SPSSODescriptor.AssertionConsumerService = {
|
|
1110
|
+
"@index": "1",
|
|
1111
|
+
"@isDefault": "true",
|
|
1112
|
+
"@Binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
1113
|
+
"@Location": this.getCallbackUrl(),
|
|
1114
|
+
};
|
|
1115
|
+
return (0, xml_1.buildXmlBuilderObject)(metadata, true);
|
|
1116
|
+
}
|
|
1117
|
+
_keyToPEM(key) {
|
|
1118
|
+
key = (0, utility_1.assertRequired)(key, "key is required");
|
|
1119
|
+
if (typeof key !== "string")
|
|
1120
|
+
return key;
|
|
1121
|
+
if (key.split(/\r?\n/).length !== 1)
|
|
1122
|
+
return key;
|
|
1123
|
+
const matchedKey = key.match(/.{1,64}/g);
|
|
1124
|
+
if (matchedKey) {
|
|
1125
|
+
const wrappedKey = [
|
|
1126
|
+
"-----BEGIN PRIVATE KEY-----",
|
|
1127
|
+
...matchedKey,
|
|
1128
|
+
"-----END PRIVATE KEY-----",
|
|
1129
|
+
"",
|
|
1130
|
+
].join("\n");
|
|
1131
|
+
return wrappedKey;
|
|
1132
|
+
}
|
|
1133
|
+
throw new Error("Invalid key");
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Process max age assertion and use it if it is more restrictive than the NotOnOrAfter age
|
|
1137
|
+
* assertion received in the SAMLResponse.
|
|
1138
|
+
*
|
|
1139
|
+
* @param maxAssertionAgeMs Max time after IssueInstant that we will accept assertion, in Ms.
|
|
1140
|
+
* @param notOnOrAfter Expiration provided in response.
|
|
1141
|
+
* @param issueInstant Time when response was issued.
|
|
1142
|
+
* @returns {*} The expiration time to be used, in Ms.
|
|
1143
|
+
*/
|
|
1144
|
+
processMaxAgeAssertionTime(maxAssertionAgeMs, notOnOrAfter, issueInstant) {
|
|
1145
|
+
const notOnOrAfterMs = this.dateStringToTimestamp(notOnOrAfter, "NotOnOrAfter");
|
|
1146
|
+
const issueInstantMs = this.dateStringToTimestamp(issueInstant, "IssueInstant");
|
|
1147
|
+
if (maxAssertionAgeMs === 0) {
|
|
1148
|
+
return notOnOrAfterMs;
|
|
1149
|
+
}
|
|
1150
|
+
const maxAssertionTimeMs = issueInstantMs + maxAssertionAgeMs;
|
|
1151
|
+
return maxAssertionTimeMs < notOnOrAfterMs ? maxAssertionTimeMs : notOnOrAfterMs;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Convert a date string to a timestamp (in milliseconds).
|
|
1155
|
+
*
|
|
1156
|
+
* @param dateString A string representation of a date
|
|
1157
|
+
* @param label Descriptive name of the date being passed in, e.g. "NotOnOrAfter"
|
|
1158
|
+
* @throws Will throw an error if parsing `dateString` returns `NaN`
|
|
1159
|
+
* @returns {number} The timestamp (in milliseconds) representation of the given date
|
|
1160
|
+
*/
|
|
1161
|
+
dateStringToTimestamp(dateString, label) {
|
|
1162
|
+
const dateMs = Date.parse(dateString);
|
|
1163
|
+
if (isNaN(dateMs)) {
|
|
1164
|
+
throw new Error(`Error parsing ${label}: '${dateString}' is not a valid date`);
|
|
1165
|
+
}
|
|
1166
|
+
return dateMs;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
exports.SAML = SAML;
|
|
1170
|
+
//# sourceMappingURL=saml.js.map
|