@tachybase/plugin-auth-saml 0.23.8
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/.turbo/turbo-build.log +14 -0
- package/README.md +11 -0
- package/README.zh-CN.md +55 -0
- package/client.d.ts +2 -0
- package/client.js +1 -0
- package/dist/client/Options.d.ts +2 -0
- package/dist/client/SAMLButton.d.ts +5 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +3 -0
- package/dist/client/locale/index.d.ts +3 -0
- package/dist/client/schemas/saml.d.ts +35 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +31 -0
- package/dist/externalVersion.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +39 -0
- package/dist/locale/en-US.json +26 -0
- package/dist/locale/es-ES.json +22 -0
- package/dist/locale/fr-FR.json +22 -0
- package/dist/locale/ko_KR.json +29 -0
- package/dist/locale/pt-BR.json +22 -0
- package/dist/locale/zh-CN.json +29 -0
- package/dist/node_modules/@node-saml/node-saml/LICENSE +23 -0
- package/dist/node_modules/@node-saml/node-saml/lib/algorithms.d.ts +5 -0
- package/dist/node_modules/@node-saml/node-saml/lib/algorithms.js +41 -0
- package/dist/node_modules/@node-saml/node-saml/lib/crypto.d.ts +5 -0
- package/dist/node_modules/@node-saml/node-saml/lib/crypto.js +48 -0
- package/dist/node_modules/@node-saml/node-saml/lib/datetime.d.ts +13 -0
- package/dist/node_modules/@node-saml/node-saml/lib/datetime.js +27 -0
- package/dist/node_modules/@node-saml/node-saml/lib/index.d.ts +3 -0
- package/dist/node_modules/@node-saml/node-saml/lib/index.js +9 -0
- package/dist/node_modules/@node-saml/node-saml/lib/inmemory-cache-provider.d.ts +38 -0
- package/dist/node_modules/@node-saml/node-saml/lib/inmemory-cache-provider.js +100 -0
- package/dist/node_modules/@node-saml/node-saml/lib/metadata.d.ts +2 -0
- package/dist/node_modules/@node-saml/node-saml/lib/metadata.js +112 -0
- package/dist/node_modules/@node-saml/node-saml/lib/passport-saml-types.d.ts +8 -0
- package/dist/node_modules/@node-saml/node-saml/lib/passport-saml-types.js +3 -0
- package/dist/node_modules/@node-saml/node-saml/lib/saml-post-signing.d.ts +3 -0
- package/dist/node_modules/@node-saml/node-saml/lib/saml-post-signing.js +15 -0
- package/dist/node_modules/@node-saml/node-saml/lib/saml.d.ts +75 -0
- package/dist/node_modules/@node-saml/node-saml/lib/saml.js +1005 -0
- package/dist/node_modules/@node-saml/node-saml/lib/types.d.ts +219 -0
- package/dist/node_modules/@node-saml/node-saml/lib/types.js +21 -0
- package/dist/node_modules/@node-saml/node-saml/lib/utility.d.ts +5 -0
- package/dist/node_modules/@node-saml/node-saml/lib/utility.js +27 -0
- package/dist/node_modules/@node-saml/node-saml/lib/xml.d.ts +26 -0
- package/dist/node_modules/@node-saml/node-saml/lib/xml.js +234 -0
- package/dist/node_modules/@node-saml/node-saml/package.json +1 -0
- package/dist/server/actions/getAuthUrl.d.ts +2 -0
- package/dist/server/actions/getAuthUrl.js +35 -0
- package/dist/server/actions/metadata.d.ts +2 -0
- package/dist/server/actions/metadata.js +36 -0
- package/dist/server/actions/redirect.d.ts +2 -0
- package/dist/server/actions/redirect.js +49 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +33 -0
- package/dist/server/migrations/20231008112900-update-autosignup.d.ts +6 -0
- package/dist/server/migrations/20231008112900-update-autosignup.js +52 -0
- package/dist/server/plugin.d.ts +11 -0
- package/dist/server/plugin.js +70 -0
- package/dist/server/saml-auth.d.ts +8 -0
- package/dist/server/saml-auth.js +110 -0
- package/dist/swagger/index.d.ts +137 -0
- package/dist/swagger/index.js +163 -0
- package/package.json +35 -0
- package/server.d.ts +2 -0
- package/server.js +1 -0
|
@@ -0,0 +1,1005 @@
|
|
|
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 types_1 = require("./types");
|
|
14
|
+
const utility_1 = require("./utility");
|
|
15
|
+
const xml_1 = require("./xml");
|
|
16
|
+
const crypto_1 = require("./crypto");
|
|
17
|
+
const datetime_1 = require("./datetime");
|
|
18
|
+
const saml_post_signing_1 = require("./saml-post-signing");
|
|
19
|
+
const metadata_1 = require("./metadata");
|
|
20
|
+
const inflateRawAsync = util.promisify(zlib.inflateRaw);
|
|
21
|
+
const deflateRawAsync = util.promisify(zlib.deflateRaw);
|
|
22
|
+
class SAML {
|
|
23
|
+
constructor(ctorOptions) {
|
|
24
|
+
this.options = this.initialize(ctorOptions);
|
|
25
|
+
this.cacheProvider = this.options.cacheProvider;
|
|
26
|
+
}
|
|
27
|
+
initialize(ctorOptions) {
|
|
28
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
|
|
29
|
+
if (!ctorOptions) {
|
|
30
|
+
throw new TypeError("SamlOptions required on construction");
|
|
31
|
+
}
|
|
32
|
+
(0, utility_1.assertRequired)(ctorOptions.issuer, "issuer is required");
|
|
33
|
+
(0, utility_1.assertRequired)(ctorOptions.cert, "cert is required");
|
|
34
|
+
// Prevent a JS user from passing in "false", which is truthy, and doing the wrong thing
|
|
35
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.passive);
|
|
36
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.disableRequestedAuthnContext);
|
|
37
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.forceAuthn);
|
|
38
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.skipRequestCompression);
|
|
39
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.disableRequestAcsUrl);
|
|
40
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.allowCreate);
|
|
41
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.wantAssertionsSigned);
|
|
42
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.wantAuthnResponseSigned);
|
|
43
|
+
(0, utility_1.assertBooleanIfPresent)(ctorOptions.signMetadata);
|
|
44
|
+
const options = {
|
|
45
|
+
...ctorOptions,
|
|
46
|
+
passive: (_a = ctorOptions.passive) !== null && _a !== void 0 ? _a : false,
|
|
47
|
+
disableRequestedAuthnContext: (_b = ctorOptions.disableRequestedAuthnContext) !== null && _b !== void 0 ? _b : false,
|
|
48
|
+
additionalParams: (_c = ctorOptions.additionalParams) !== null && _c !== void 0 ? _c : {},
|
|
49
|
+
additionalAuthorizeParams: (_d = ctorOptions.additionalAuthorizeParams) !== null && _d !== void 0 ? _d : {},
|
|
50
|
+
additionalLogoutParams: (_e = ctorOptions.additionalLogoutParams) !== null && _e !== void 0 ? _e : {},
|
|
51
|
+
forceAuthn: (_f = ctorOptions.forceAuthn) !== null && _f !== void 0 ? _f : false,
|
|
52
|
+
skipRequestCompression: (_g = ctorOptions.skipRequestCompression) !== null && _g !== void 0 ? _g : false,
|
|
53
|
+
disableRequestAcsUrl: (_h = ctorOptions.disableRequestAcsUrl) !== null && _h !== void 0 ? _h : false,
|
|
54
|
+
acceptedClockSkewMs: (_j = ctorOptions.acceptedClockSkewMs) !== null && _j !== void 0 ? _j : 0,
|
|
55
|
+
maxAssertionAgeMs: (_k = ctorOptions.maxAssertionAgeMs) !== null && _k !== void 0 ? _k : 0,
|
|
56
|
+
path: (_l = ctorOptions.path) !== null && _l !== void 0 ? _l : "/saml/consume",
|
|
57
|
+
host: (_m = ctorOptions.host) !== null && _m !== void 0 ? _m : "localhost",
|
|
58
|
+
issuer: ctorOptions.issuer,
|
|
59
|
+
audience: (_p = (_o = ctorOptions.audience) !== null && _o !== void 0 ? _o : ctorOptions.issuer) !== null && _p !== void 0 ? _p : "unknown_audience",
|
|
60
|
+
identifierFormat: ctorOptions.identifierFormat === undefined
|
|
61
|
+
? "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
|
62
|
+
: ctorOptions.identifierFormat,
|
|
63
|
+
allowCreate: (_q = ctorOptions.allowCreate) !== null && _q !== void 0 ? _q : true,
|
|
64
|
+
spNameQualifier: ctorOptions.spNameQualifier,
|
|
65
|
+
wantAssertionsSigned: (_r = ctorOptions.wantAssertionsSigned) !== null && _r !== void 0 ? _r : true,
|
|
66
|
+
wantAuthnResponseSigned: (_s = ctorOptions.wantAuthnResponseSigned) !== null && _s !== void 0 ? _s : true,
|
|
67
|
+
authnContext: (_t = ctorOptions.authnContext) !== null && _t !== void 0 ? _t : [
|
|
68
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
|
|
69
|
+
],
|
|
70
|
+
validateInResponseTo: (_u = ctorOptions.validateInResponseTo) !== null && _u !== void 0 ? _u : types_1.ValidateInResponseTo.never,
|
|
71
|
+
cert: ctorOptions.cert,
|
|
72
|
+
requestIdExpirationPeriodMs: (_v = ctorOptions.requestIdExpirationPeriodMs) !== null && _v !== void 0 ? _v : 28800000,
|
|
73
|
+
cacheProvider: (_w = ctorOptions.cacheProvider) !== null && _w !== void 0 ? _w : new inmemory_cache_provider_1.InMemoryCacheProvider({
|
|
74
|
+
keyExpirationPeriodMs: ctorOptions.requestIdExpirationPeriodMs,
|
|
75
|
+
}),
|
|
76
|
+
logoutUrl: (_y = (_x = ctorOptions.logoutUrl) !== null && _x !== void 0 ? _x : ctorOptions.entryPoint) !== null && _y !== void 0 ? _y : "",
|
|
77
|
+
signatureAlgorithm: (_z = ctorOptions.signatureAlgorithm) !== null && _z !== void 0 ? _z : "sha1",
|
|
78
|
+
authnRequestBinding: (_0 = ctorOptions.authnRequestBinding) !== null && _0 !== void 0 ? _0 : "HTTP-Redirect",
|
|
79
|
+
generateUniqueId: (_1 = ctorOptions.generateUniqueId) !== null && _1 !== void 0 ? _1 : crypto_1.generateUniqueId,
|
|
80
|
+
signMetadata: (_2 = ctorOptions.signMetadata) !== null && _2 !== void 0 ? _2 : false,
|
|
81
|
+
racComparison: (_3 = ctorOptions.racComparison) !== null && _3 !== void 0 ? _3 : "exact",
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* List of possible values:
|
|
85
|
+
* - exact : Assertion context must exactly match a context in the list
|
|
86
|
+
* - minimum: Assertion context must be at least as strong as a context in the list
|
|
87
|
+
* - maximum: Assertion context must be no stronger than a context in the list
|
|
88
|
+
* - better: Assertion context must be stronger than all contexts in the list
|
|
89
|
+
*/
|
|
90
|
+
if (!["exact", "minimum", "maximum", "better"].includes(options.racComparison)) {
|
|
91
|
+
throw new TypeError("racComparison must be one of ['exact', 'minimum', 'maximum', 'better']");
|
|
92
|
+
}
|
|
93
|
+
return options;
|
|
94
|
+
}
|
|
95
|
+
getCallbackUrl(host) {
|
|
96
|
+
// Post-auth destination
|
|
97
|
+
if (this.options.callbackUrl) {
|
|
98
|
+
return this.options.callbackUrl;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const url = new url_1.URL("http://localhost");
|
|
102
|
+
if (host) {
|
|
103
|
+
url.host = host;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
url.host = this.options.host;
|
|
107
|
+
}
|
|
108
|
+
if (this.options.protocol) {
|
|
109
|
+
url.protocol = this.options.protocol;
|
|
110
|
+
}
|
|
111
|
+
url.pathname = this.options.path;
|
|
112
|
+
return url.toString();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
signRequest(samlMessage) {
|
|
116
|
+
(0, utility_1.assertRequired)(this.options.privateKey, "privateKey is required");
|
|
117
|
+
const samlMessageToSign = {};
|
|
118
|
+
samlMessage.SigAlg = algorithms.getSigningAlgorithm(this.options.signatureAlgorithm);
|
|
119
|
+
const signer = algorithms.getSigner(this.options.signatureAlgorithm);
|
|
120
|
+
if (samlMessage.SAMLRequest) {
|
|
121
|
+
samlMessageToSign.SAMLRequest = samlMessage.SAMLRequest;
|
|
122
|
+
}
|
|
123
|
+
if (samlMessage.SAMLResponse) {
|
|
124
|
+
samlMessageToSign.SAMLResponse = samlMessage.SAMLResponse;
|
|
125
|
+
}
|
|
126
|
+
if (samlMessage.RelayState) {
|
|
127
|
+
samlMessageToSign.RelayState = samlMessage.RelayState;
|
|
128
|
+
}
|
|
129
|
+
if (samlMessage.SigAlg) {
|
|
130
|
+
samlMessageToSign.SigAlg = samlMessage.SigAlg;
|
|
131
|
+
}
|
|
132
|
+
signer.update(querystring.stringify(samlMessageToSign));
|
|
133
|
+
samlMessage.Signature = signer.sign((0, crypto_1.keyToPEM)(this.options.privateKey), "base64");
|
|
134
|
+
}
|
|
135
|
+
async generateAuthorizeRequestAsync(isPassive, isHttpPostBinding, host) {
|
|
136
|
+
(0, utility_1.assertRequired)(this.options.entryPoint, "entryPoint is required");
|
|
137
|
+
const id = this.options.generateUniqueId();
|
|
138
|
+
const instant = (0, datetime_1.generateInstant)();
|
|
139
|
+
if (this.mustValidateInResponseTo(true)) {
|
|
140
|
+
await this.cacheProvider.saveAsync(id, instant);
|
|
141
|
+
}
|
|
142
|
+
const request = {
|
|
143
|
+
"samlp:AuthnRequest": {
|
|
144
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
145
|
+
"@ID": id,
|
|
146
|
+
"@Version": "2.0",
|
|
147
|
+
"@IssueInstant": instant,
|
|
148
|
+
"@ProtocolBinding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
149
|
+
"@Destination": this.options.entryPoint,
|
|
150
|
+
"saml:Issuer": {
|
|
151
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
152
|
+
"#text": this.options.issuer,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
if (isPassive)
|
|
157
|
+
request["samlp:AuthnRequest"]["@IsPassive"] = true;
|
|
158
|
+
if (this.options.forceAuthn === true) {
|
|
159
|
+
request["samlp:AuthnRequest"]["@ForceAuthn"] = true;
|
|
160
|
+
}
|
|
161
|
+
if (!this.options.disableRequestAcsUrl) {
|
|
162
|
+
request["samlp:AuthnRequest"]["@AssertionConsumerServiceURL"] = this.getCallbackUrl(host);
|
|
163
|
+
}
|
|
164
|
+
const samlAuthnRequestExtensions = this.options.samlAuthnRequestExtensions;
|
|
165
|
+
if (samlAuthnRequestExtensions != null) {
|
|
166
|
+
if (typeof samlAuthnRequestExtensions != "object") {
|
|
167
|
+
throw new TypeError("samlAuthnRequestExtensions should be Object");
|
|
168
|
+
}
|
|
169
|
+
request["samlp:AuthnRequest"]["samlp:Extensions"] = {
|
|
170
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
171
|
+
...samlAuthnRequestExtensions,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const nameIDPolicy = {
|
|
175
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
176
|
+
"@AllowCreate": this.options.allowCreate,
|
|
177
|
+
};
|
|
178
|
+
if (this.options.identifierFormat != null) {
|
|
179
|
+
nameIDPolicy["@Format"] = this.options.identifierFormat;
|
|
180
|
+
}
|
|
181
|
+
if (this.options.spNameQualifier != null) {
|
|
182
|
+
nameIDPolicy["@SPNameQualifier"] = this.options.spNameQualifier;
|
|
183
|
+
}
|
|
184
|
+
request["samlp:AuthnRequest"]["samlp:NameIDPolicy"] = nameIDPolicy;
|
|
185
|
+
if (!this.options.disableRequestedAuthnContext) {
|
|
186
|
+
const authnContextClassRefs = [];
|
|
187
|
+
this.options.authnContext.forEach(function (value) {
|
|
188
|
+
authnContextClassRefs.push({
|
|
189
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
190
|
+
"#text": value,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
request["samlp:AuthnRequest"]["samlp:RequestedAuthnContext"] = {
|
|
194
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
195
|
+
"@Comparison": this.options.racComparison,
|
|
196
|
+
"saml:AuthnContextClassRef": authnContextClassRefs,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (this.options.attributeConsumingServiceIndex != null) {
|
|
200
|
+
request["samlp:AuthnRequest"]["@AttributeConsumingServiceIndex"] =
|
|
201
|
+
this.options.attributeConsumingServiceIndex;
|
|
202
|
+
}
|
|
203
|
+
if (this.options.providerName != null) {
|
|
204
|
+
request["samlp:AuthnRequest"]["@ProviderName"] = this.options.providerName;
|
|
205
|
+
}
|
|
206
|
+
if (this.options.scoping != null) {
|
|
207
|
+
const scoping = {
|
|
208
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
209
|
+
};
|
|
210
|
+
if (typeof this.options.scoping.proxyCount === "number") {
|
|
211
|
+
scoping["@ProxyCount"] = this.options.scoping.proxyCount;
|
|
212
|
+
}
|
|
213
|
+
if (this.options.scoping.idpList) {
|
|
214
|
+
scoping["samlp:IDPList"] = this.options.scoping.idpList.map((idpListItem) => {
|
|
215
|
+
const formattedIdpListItem = {
|
|
216
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
217
|
+
};
|
|
218
|
+
if (idpListItem.entries) {
|
|
219
|
+
formattedIdpListItem["samlp:IDPEntry"] = idpListItem.entries.map((entry) => {
|
|
220
|
+
const formattedEntry = {
|
|
221
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
222
|
+
};
|
|
223
|
+
formattedEntry["@ProviderID"] = entry.providerId;
|
|
224
|
+
if (entry.name) {
|
|
225
|
+
formattedEntry["@Name"] = entry.name;
|
|
226
|
+
}
|
|
227
|
+
if (entry.loc) {
|
|
228
|
+
formattedEntry["@Loc"] = entry.loc;
|
|
229
|
+
}
|
|
230
|
+
return formattedEntry;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (idpListItem.getComplete) {
|
|
234
|
+
formattedIdpListItem["samlp:GetComplete"] = idpListItem.getComplete;
|
|
235
|
+
}
|
|
236
|
+
return formattedIdpListItem;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (this.options.scoping.requesterId) {
|
|
240
|
+
scoping["samlp:RequesterID"] = this.options.scoping.requesterId;
|
|
241
|
+
}
|
|
242
|
+
request["samlp:AuthnRequest"]["samlp:Scoping"] = scoping;
|
|
243
|
+
}
|
|
244
|
+
let stringRequest = (0, xml_1.buildXmlBuilderObject)(request, false);
|
|
245
|
+
// TODO: maybe we should always sign here
|
|
246
|
+
if (isHttpPostBinding && (0, types_1.isValidSamlSigningOptions)(this.options)) {
|
|
247
|
+
stringRequest = (0, saml_post_signing_1.signAuthnRequestPost)(stringRequest, this.options);
|
|
248
|
+
}
|
|
249
|
+
return stringRequest;
|
|
250
|
+
}
|
|
251
|
+
async _generateLogoutRequest(user) {
|
|
252
|
+
const id = this.options.generateUniqueId();
|
|
253
|
+
const instant = (0, datetime_1.generateInstant)();
|
|
254
|
+
const request = {
|
|
255
|
+
"samlp:LogoutRequest": {
|
|
256
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
257
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
258
|
+
"@ID": id,
|
|
259
|
+
"@Version": "2.0",
|
|
260
|
+
"@IssueInstant": instant,
|
|
261
|
+
"@Destination": this.options.logoutUrl,
|
|
262
|
+
"saml:Issuer": {
|
|
263
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
264
|
+
"#text": this.options.issuer,
|
|
265
|
+
},
|
|
266
|
+
"samlp:Extensions": {},
|
|
267
|
+
"saml:NameID": {
|
|
268
|
+
"@Format": user.nameIDFormat,
|
|
269
|
+
"#text": user.nameID,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const samlLogoutRequestExtensions = this.options.samlLogoutRequestExtensions;
|
|
274
|
+
if (samlLogoutRequestExtensions != null) {
|
|
275
|
+
if (typeof samlLogoutRequestExtensions != "object") {
|
|
276
|
+
throw new TypeError("samlLogoutRequestExtensions should be Object");
|
|
277
|
+
}
|
|
278
|
+
request["samlp:LogoutRequest"]["samlp:Extensions"] = {
|
|
279
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
280
|
+
...samlLogoutRequestExtensions,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
delete request["samlp:LogoutRequest"]["samlp:Extensions"];
|
|
285
|
+
}
|
|
286
|
+
if (user.nameQualifier != null) {
|
|
287
|
+
request["samlp:LogoutRequest"]["saml:NameID"]["@NameQualifier"] = user.nameQualifier;
|
|
288
|
+
}
|
|
289
|
+
if (user.spNameQualifier != null) {
|
|
290
|
+
request["samlp:LogoutRequest"]["saml:NameID"]["@SPNameQualifier"] = user.spNameQualifier;
|
|
291
|
+
}
|
|
292
|
+
if (user.sessionIndex) {
|
|
293
|
+
request["samlp:LogoutRequest"]["saml2p:SessionIndex"] = {
|
|
294
|
+
"@xmlns:saml2p": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
295
|
+
"#text": user.sessionIndex,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
await this.cacheProvider.saveAsync(id, instant);
|
|
299
|
+
return (0, xml_1.buildXmlBuilderObject)(request, false);
|
|
300
|
+
}
|
|
301
|
+
_generateLogoutResponse(logoutRequest, success) {
|
|
302
|
+
const id = this.options.generateUniqueId();
|
|
303
|
+
const instant = (0, datetime_1.generateInstant)();
|
|
304
|
+
const successStatus = {
|
|
305
|
+
"samlp:StatusCode": {
|
|
306
|
+
"@Value": "urn:oasis:names:tc:SAML:2.0:status:Success",
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
const failStatus = {
|
|
310
|
+
"samlp:StatusCode": {
|
|
311
|
+
"@Value": "urn:oasis:names:tc:SAML:2.0:status:Requester",
|
|
312
|
+
"samlp:StatusCode": {
|
|
313
|
+
"@Value": "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
const request = {
|
|
318
|
+
"samlp:LogoutResponse": {
|
|
319
|
+
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
320
|
+
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
321
|
+
"@ID": id,
|
|
322
|
+
"@Version": "2.0",
|
|
323
|
+
"@IssueInstant": instant,
|
|
324
|
+
"@Destination": this.options.logoutUrl,
|
|
325
|
+
"@InResponseTo": logoutRequest.ID,
|
|
326
|
+
"saml:Issuer": {
|
|
327
|
+
"#text": this.options.issuer,
|
|
328
|
+
},
|
|
329
|
+
"samlp:Status": success ? successStatus : failStatus,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
return (0, xml_1.buildXmlBuilderObject)(request, false);
|
|
333
|
+
}
|
|
334
|
+
async _requestToUrlAsync(request, response, operation, additionalParameters) {
|
|
335
|
+
(0, utility_1.assertRequired)(this.options.entryPoint, "entryPoint is required");
|
|
336
|
+
const requestOrResponse = request || response;
|
|
337
|
+
(0, utility_1.assertRequired)(requestOrResponse, "either request or response is required");
|
|
338
|
+
let buffer;
|
|
339
|
+
if (this.options.skipRequestCompression) {
|
|
340
|
+
buffer = Buffer.from(requestOrResponse, "utf8");
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
buffer = await deflateRawAsync(requestOrResponse);
|
|
344
|
+
}
|
|
345
|
+
const base64 = buffer.toString("base64");
|
|
346
|
+
let target = new url_1.URL(this.options.entryPoint);
|
|
347
|
+
if (operation === "logout") {
|
|
348
|
+
if (this.options.logoutUrl) {
|
|
349
|
+
target = new url_1.URL(this.options.logoutUrl);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (operation !== "authorize") {
|
|
353
|
+
throw new Error("Unknown operation: " + operation);
|
|
354
|
+
}
|
|
355
|
+
const samlMessage = request
|
|
356
|
+
? {
|
|
357
|
+
SAMLRequest: base64,
|
|
358
|
+
}
|
|
359
|
+
: {
|
|
360
|
+
SAMLResponse: base64,
|
|
361
|
+
};
|
|
362
|
+
Object.keys(additionalParameters).forEach((k) => {
|
|
363
|
+
samlMessage[k] = additionalParameters[k];
|
|
364
|
+
});
|
|
365
|
+
if ((0, types_1.isValidSamlSigningOptions)(this.options)) {
|
|
366
|
+
if (!this.options.entryPoint) {
|
|
367
|
+
throw new Error('"entryPoint" config parameter is required for signed messages');
|
|
368
|
+
}
|
|
369
|
+
// sets .SigAlg and .Signature
|
|
370
|
+
this.signRequest(samlMessage);
|
|
371
|
+
}
|
|
372
|
+
Object.keys(samlMessage).forEach((k) => {
|
|
373
|
+
target.searchParams.set(k, samlMessage[k]);
|
|
374
|
+
});
|
|
375
|
+
return target.toString();
|
|
376
|
+
}
|
|
377
|
+
_getAdditionalParams(relayState, operation, overrideParams) {
|
|
378
|
+
const additionalParams = {};
|
|
379
|
+
if (typeof relayState === "string" && relayState.length > 0) {
|
|
380
|
+
additionalParams.RelayState = relayState;
|
|
381
|
+
}
|
|
382
|
+
return Object.assign(additionalParams, this.options.additionalParams, operation === "logout"
|
|
383
|
+
? this.options.additionalLogoutParams
|
|
384
|
+
: this.options.additionalAuthorizeParams, overrideParams !== null && overrideParams !== void 0 ? overrideParams : {});
|
|
385
|
+
}
|
|
386
|
+
async getAuthorizeUrlAsync(RelayState, host, options) {
|
|
387
|
+
const request = await this.generateAuthorizeRequestAsync(this.options.passive, false, host);
|
|
388
|
+
const operation = "authorize";
|
|
389
|
+
const overrideParams = options ? options.additionalParams || {} : {};
|
|
390
|
+
return await this._requestToUrlAsync(request, null, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
391
|
+
}
|
|
392
|
+
async getAuthorizeFormAsync(RelayState, host) {
|
|
393
|
+
(0, utility_1.assertRequired)(this.options.entryPoint, "entryPoint is required");
|
|
394
|
+
// The quoteattr() function is used in a context, where the result will not be evaluated by javascript
|
|
395
|
+
// but must be interpreted by an XML or HTML parser, and it must absolutely avoid breaking the syntax
|
|
396
|
+
// of an element attribute.
|
|
397
|
+
const quoteattr = function (s, preserveCR) {
|
|
398
|
+
const preserveCRChar = preserveCR ? " " : "\n";
|
|
399
|
+
return (("" + s) // Forces the conversion to string.
|
|
400
|
+
.replace(/&/g, "&") // This MUST be the 1st replacement.
|
|
401
|
+
.replace(/'/g, "'") // The 4 other predefined entities, required.
|
|
402
|
+
.replace(/"/g, """)
|
|
403
|
+
.replace(/</g, "<")
|
|
404
|
+
.replace(/>/g, ">")
|
|
405
|
+
// Add other replacements here for HTML only
|
|
406
|
+
// Or for XML, only if the named entities are defined in its DTD.
|
|
407
|
+
.replace(/\r\n/g, preserveCRChar) // Must be before the next replacement.
|
|
408
|
+
.replace(/[\r\n]/g, preserveCRChar));
|
|
409
|
+
};
|
|
410
|
+
const request = await this.generateAuthorizeRequestAsync(this.options.passive, true, host);
|
|
411
|
+
let buffer;
|
|
412
|
+
if (this.options.skipRequestCompression) {
|
|
413
|
+
buffer = Buffer.from(request, "utf8");
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
buffer = await deflateRawAsync(request);
|
|
417
|
+
}
|
|
418
|
+
const operation = "authorize";
|
|
419
|
+
const additionalParameters = this._getAdditionalParams(RelayState, operation);
|
|
420
|
+
const samlMessage = {
|
|
421
|
+
SAMLRequest: buffer.toString("base64"),
|
|
422
|
+
};
|
|
423
|
+
Object.keys(additionalParameters).forEach((k) => {
|
|
424
|
+
samlMessage[k] = additionalParameters[k] || "";
|
|
425
|
+
});
|
|
426
|
+
const formInputs = Object.keys(samlMessage)
|
|
427
|
+
.map((k) => {
|
|
428
|
+
return '<input type="hidden" name="' + k + '" value="' + quoteattr(samlMessage[k]) + '" />';
|
|
429
|
+
})
|
|
430
|
+
.join("\r\n");
|
|
431
|
+
return [
|
|
432
|
+
"<!DOCTYPE html>",
|
|
433
|
+
"<html>",
|
|
434
|
+
"<head>",
|
|
435
|
+
'<meta charset="utf-8">',
|
|
436
|
+
'<meta http-equiv="x-ua-compatible" content="ie=edge">',
|
|
437
|
+
"</head>",
|
|
438
|
+
'<body onload="document.forms[0].submit()">',
|
|
439
|
+
"<noscript>",
|
|
440
|
+
"<p><strong>Note:</strong> Since your browser does not support JavaScript, you must press the button below once to proceed.</p>",
|
|
441
|
+
"</noscript>",
|
|
442
|
+
'<form method="post" action="' + encodeURI(this.options.entryPoint) + '">',
|
|
443
|
+
formInputs,
|
|
444
|
+
'<input type="submit" value="Submit" />',
|
|
445
|
+
"</form>",
|
|
446
|
+
'<script>document.forms[0].style.display="none";</script>',
|
|
447
|
+
"</body>",
|
|
448
|
+
"</html>",
|
|
449
|
+
].join("\r\n");
|
|
450
|
+
}
|
|
451
|
+
async getLogoutUrlAsync(user, RelayState, options) {
|
|
452
|
+
const request = await this._generateLogoutRequest(user);
|
|
453
|
+
const operation = "logout";
|
|
454
|
+
const overrideParams = options ? options.additionalParams || {} : {};
|
|
455
|
+
return await this._requestToUrlAsync(request, null, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
456
|
+
}
|
|
457
|
+
getLogoutResponseUrl(samlLogoutRequest, RelayState, options, success, callback) {
|
|
458
|
+
util.callbackify(() => this.getLogoutResponseUrlAsync(samlLogoutRequest, RelayState, options, success))(callback);
|
|
459
|
+
}
|
|
460
|
+
async getLogoutResponseUrlAsync(samlLogoutRequest, RelayState, options, success) {
|
|
461
|
+
const response = this._generateLogoutResponse(samlLogoutRequest, success);
|
|
462
|
+
const operation = "logout";
|
|
463
|
+
const overrideParams = options ? options.additionalParams || {} : {};
|
|
464
|
+
return await this._requestToUrlAsync(null, response, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
465
|
+
}
|
|
466
|
+
async certsToCheck() {
|
|
467
|
+
let checkedCerts;
|
|
468
|
+
if (typeof this.options.cert === "function") {
|
|
469
|
+
checkedCerts = await util
|
|
470
|
+
.promisify(this.options.cert)()
|
|
471
|
+
.then((certs) => {
|
|
472
|
+
(0, utility_1.assertRequired)(certs, "callback didn't return cert");
|
|
473
|
+
if (!Array.isArray(certs)) {
|
|
474
|
+
certs = [certs];
|
|
475
|
+
}
|
|
476
|
+
return certs;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
else if (Array.isArray(this.options.cert)) {
|
|
480
|
+
checkedCerts = this.options.cert;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
checkedCerts = [this.options.cert];
|
|
484
|
+
}
|
|
485
|
+
checkedCerts.forEach((cert) => {
|
|
486
|
+
(0, utility_1.assertRequired)(cert, "unknown cert found");
|
|
487
|
+
});
|
|
488
|
+
return checkedCerts;
|
|
489
|
+
}
|
|
490
|
+
async validatePostResponseAsync(container) {
|
|
491
|
+
var _a, _b, _c, _d;
|
|
492
|
+
let xml;
|
|
493
|
+
let doc;
|
|
494
|
+
let inResponseTo = null;
|
|
495
|
+
try {
|
|
496
|
+
xml = Buffer.from(container.SAMLResponse, "base64").toString("utf8");
|
|
497
|
+
doc = await (0, xml_1.parseDomFromString)(xml);
|
|
498
|
+
const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
|
|
499
|
+
if (inResponseToNodes) {
|
|
500
|
+
inResponseTo = inResponseToNodes.length ? inResponseToNodes[0].nodeValue : null;
|
|
501
|
+
await this.validateInResponseTo(inResponseTo);
|
|
502
|
+
}
|
|
503
|
+
const certs = await this.certsToCheck();
|
|
504
|
+
// Check if this document has a valid top-level signature which applies to the entire XML document
|
|
505
|
+
let validSignature = false;
|
|
506
|
+
if ((0, xml_1.validateSignature)(xml, doc.documentElement, certs)) {
|
|
507
|
+
validSignature = true;
|
|
508
|
+
}
|
|
509
|
+
if (this.options.wantAuthnResponseSigned === true && validSignature === false) {
|
|
510
|
+
throw new Error("Invalid document signature");
|
|
511
|
+
}
|
|
512
|
+
const assertions = xml_1.xpath.selectElements(doc, "/*[local-name()='Response']/*[local-name()='Assertion']");
|
|
513
|
+
const encryptedAssertions = xml_1.xpath.selectElements(doc, "/*[local-name()='Response']/*[local-name()='EncryptedAssertion']");
|
|
514
|
+
if (assertions.length + encryptedAssertions.length > 1) {
|
|
515
|
+
// There's no reason I know of that we want to handle multiple assertions, and it seems like a
|
|
516
|
+
// potential risk vector for signature scope issues, so treat this as an invalid signature
|
|
517
|
+
throw new Error("Invalid signature: multiple assertions");
|
|
518
|
+
}
|
|
519
|
+
if (assertions.length == 1) {
|
|
520
|
+
if ((this.options.wantAssertionsSigned || !validSignature) &&
|
|
521
|
+
!(0, xml_1.validateSignature)(xml, assertions[0], certs)) {
|
|
522
|
+
throw new Error("Invalid signature");
|
|
523
|
+
}
|
|
524
|
+
return await this.processValidlySignedAssertionAsync(assertions[0].toString(), xml, inResponseTo);
|
|
525
|
+
}
|
|
526
|
+
if (encryptedAssertions.length == 1) {
|
|
527
|
+
(0, utility_1.assertRequired)(this.options.decryptionPvk, "No decryption key for encrypted SAML response");
|
|
528
|
+
const encryptedAssertionXml = encryptedAssertions[0].toString();
|
|
529
|
+
const decryptedXml = await (0, xml_1.decryptXml)(encryptedAssertionXml, this.options.decryptionPvk);
|
|
530
|
+
const decryptedDoc = await (0, xml_1.parseDomFromString)(decryptedXml);
|
|
531
|
+
const decryptedAssertions = xml_1.xpath.selectElements(decryptedDoc, "/*[local-name()='Assertion']");
|
|
532
|
+
if (decryptedAssertions.length != 1)
|
|
533
|
+
throw new Error("Invalid EncryptedAssertion content");
|
|
534
|
+
if ((this.options.wantAssertionsSigned || !validSignature) &&
|
|
535
|
+
!(0, xml_1.validateSignature)(decryptedXml, decryptedAssertions[0], certs)) {
|
|
536
|
+
throw new Error("Invalid signature from encrypted assertion");
|
|
537
|
+
}
|
|
538
|
+
return await this.processValidlySignedAssertionAsync(decryptedAssertions[0].toString(), xml, inResponseTo);
|
|
539
|
+
}
|
|
540
|
+
// If there's no assertion, fall back on xml2js response parsing for the status &
|
|
541
|
+
// LogoutResponse code.
|
|
542
|
+
const xmljsDoc = (await (0, xml_1.parseXml2JsFromString)(xml));
|
|
543
|
+
const response = xmljsDoc.Response;
|
|
544
|
+
if (response) {
|
|
545
|
+
if (!("Assertion" in response)) {
|
|
546
|
+
const status = response.Status;
|
|
547
|
+
if (status) {
|
|
548
|
+
const statusCode = status[0].StatusCode;
|
|
549
|
+
if (statusCode &&
|
|
550
|
+
((_a = statusCode[0].$) === null || _a === void 0 ? void 0 : _a.Value) === "urn:oasis:names:tc:SAML:2.0:status:Responder") {
|
|
551
|
+
const nestedStatusCode = statusCode[0].StatusCode;
|
|
552
|
+
if (nestedStatusCode &&
|
|
553
|
+
((_b = nestedStatusCode[0].$) === null || _b === void 0 ? void 0 : _b.Value) === "urn:oasis:names:tc:SAML:2.0:status:NoPassive") {
|
|
554
|
+
if (!validSignature) {
|
|
555
|
+
throw new Error("Invalid signature: NoPassive");
|
|
556
|
+
}
|
|
557
|
+
return { profile: null, loggedOut: false };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Note that we're not requiring a valid signature before this logic -- since we are
|
|
561
|
+
// throwing an error in any case, and some providers don't sign error results,
|
|
562
|
+
// let's go ahead and give the potentially more helpful error.
|
|
563
|
+
if (statusCode && ((_c = statusCode[0].$) === null || _c === void 0 ? void 0 : _c.Value)) {
|
|
564
|
+
const msgType = statusCode[0].$.Value.match(/[^:]*$/);
|
|
565
|
+
if (msgType && msgType[0] != "Success") {
|
|
566
|
+
let msg = "unspecified";
|
|
567
|
+
if (status[0].StatusMessage) {
|
|
568
|
+
msg = status[0].StatusMessage[0]._ || msg;
|
|
569
|
+
}
|
|
570
|
+
else if (statusCode[0].StatusCode) {
|
|
571
|
+
const msgValues = (_d = statusCode[0].StatusCode[0].$) === null || _d === void 0 ? void 0 : _d.Value.match(/[^:]*$/);
|
|
572
|
+
msg = msgValues ? msgValues[0] : msg;
|
|
573
|
+
}
|
|
574
|
+
const statusXml = (0, xml_1.buildXml2JsObject)("Status", status[0]);
|
|
575
|
+
throw new types_1.ErrorWithXmlStatus("SAML provider returned " + msgType + " error: " + msg, statusXml);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
throw new Error("Missing SAML assertion");
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
if (!validSignature) {
|
|
584
|
+
throw new Error("Invalid signature: No response found");
|
|
585
|
+
}
|
|
586
|
+
const logoutResponse = xmljsDoc.LogoutResponse;
|
|
587
|
+
if (logoutResponse) {
|
|
588
|
+
return { profile: null, loggedOut: true };
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
throw new Error("Unknown SAML response message");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
debug("validatePostResponse resulted in an error: %s", err);
|
|
597
|
+
if (this.mustValidateInResponseTo(Boolean(inResponseTo))) {
|
|
598
|
+
await this.cacheProvider.removeAsync(inResponseTo);
|
|
599
|
+
}
|
|
600
|
+
throw err;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async validateInResponseTo(inResponseTo) {
|
|
604
|
+
if (this.mustValidateInResponseTo(Boolean(inResponseTo))) {
|
|
605
|
+
if (inResponseTo) {
|
|
606
|
+
const result = await this.cacheProvider.getAsync(inResponseTo);
|
|
607
|
+
if (!result)
|
|
608
|
+
throw new Error("InResponseTo is not valid");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
throw new Error("InResponseTo is missing from response");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async validateRedirectAsync(container, originalQuery) {
|
|
617
|
+
const samlMessageType = container.SAMLRequest ? "SAMLRequest" : "SAMLResponse";
|
|
618
|
+
const data = Buffer.from(container[samlMessageType], "base64");
|
|
619
|
+
const inflated = await inflateRawAsync(data);
|
|
620
|
+
const dom = await (0, xml_1.parseDomFromString)(inflated.toString());
|
|
621
|
+
const doc = await (0, xml_1.parseXml2JsFromString)(inflated);
|
|
622
|
+
samlMessageType === "SAMLResponse"
|
|
623
|
+
? await this.verifyLogoutResponse(doc)
|
|
624
|
+
: this.verifyLogoutRequest(doc);
|
|
625
|
+
await this.hasValidSignatureForRedirect(container, originalQuery);
|
|
626
|
+
return await this.processValidlySignedSamlLogoutAsync(doc, dom);
|
|
627
|
+
}
|
|
628
|
+
async hasValidSignatureForRedirect(container, originalQuery) {
|
|
629
|
+
const tokens = originalQuery.split("&");
|
|
630
|
+
const getParam = (key) => {
|
|
631
|
+
const exists = tokens.filter((t) => {
|
|
632
|
+
return new RegExp(key).test(t);
|
|
633
|
+
});
|
|
634
|
+
return exists[0];
|
|
635
|
+
};
|
|
636
|
+
if (container.Signature) {
|
|
637
|
+
let urlString = getParam("SAMLRequest") || getParam("SAMLResponse");
|
|
638
|
+
if (getParam("RelayState")) {
|
|
639
|
+
urlString += "&" + getParam("RelayState");
|
|
640
|
+
}
|
|
641
|
+
urlString += "&" + getParam("SigAlg");
|
|
642
|
+
const certs = await this.certsToCheck();
|
|
643
|
+
const hasValidQuerySignature = certs.some((cert) => {
|
|
644
|
+
return this.validateSignatureForRedirect(urlString, container.Signature, container.SigAlg, cert);
|
|
645
|
+
});
|
|
646
|
+
if (!hasValidQuerySignature) {
|
|
647
|
+
throw new Error("Invalid query signature");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
validateSignatureForRedirect(urlString, signature, alg, cert) {
|
|
655
|
+
// See if we support a matching algorithm, case-insensitive. Otherwise, throw error.
|
|
656
|
+
function hasMatch(ourAlgo) {
|
|
657
|
+
// The incoming algorithm is forwarded as a URL.
|
|
658
|
+
// We trim everything before the last # get something we can compare to the Node.js list
|
|
659
|
+
const algFromURI = alg.toLowerCase().replace(/.*#(.*)$/, "$1");
|
|
660
|
+
return ourAlgo.toLowerCase() === algFromURI;
|
|
661
|
+
}
|
|
662
|
+
const i = crypto.getHashes().findIndex(hasMatch);
|
|
663
|
+
let matchingAlgo;
|
|
664
|
+
if (i > -1) {
|
|
665
|
+
matchingAlgo = crypto.getHashes()[i];
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
throw new Error(alg + " is not supported");
|
|
669
|
+
}
|
|
670
|
+
const verifier = crypto.createVerify(matchingAlgo);
|
|
671
|
+
verifier.update(urlString);
|
|
672
|
+
return verifier.verify((0, crypto_1.certToPEM)(cert), signature, "base64");
|
|
673
|
+
}
|
|
674
|
+
verifyLogoutRequest(doc) {
|
|
675
|
+
this.verifyIssuer(doc.LogoutRequest);
|
|
676
|
+
const nowMs = new Date().getTime();
|
|
677
|
+
const conditions = doc.LogoutRequest.$;
|
|
678
|
+
const conErr = this.checkTimestampsValidityError(nowMs, conditions.NotBefore, conditions.NotOnOrAfter);
|
|
679
|
+
if (conErr) {
|
|
680
|
+
throw conErr;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async verifyLogoutResponse(doc) {
|
|
684
|
+
const statusCode = doc.LogoutResponse.Status[0].StatusCode[0].$.Value;
|
|
685
|
+
if (statusCode !== "urn:oasis:names:tc:SAML:2.0:status:Success")
|
|
686
|
+
throw new Error("Bad status code: " + statusCode);
|
|
687
|
+
this.verifyIssuer(doc.LogoutResponse);
|
|
688
|
+
const inResponseTo = doc.LogoutResponse.$.InResponseTo;
|
|
689
|
+
if (inResponseTo) {
|
|
690
|
+
return this.validateInResponseTo(inResponseTo);
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
verifyIssuer(samlMessage) {
|
|
695
|
+
if (this.options.idpIssuer != null) {
|
|
696
|
+
const issuer = samlMessage.Issuer;
|
|
697
|
+
if (issuer) {
|
|
698
|
+
if (issuer[0]._ !== this.options.idpIssuer)
|
|
699
|
+
throw new Error("Unknown SAML issuer. Expected: " + this.options.idpIssuer + " Received: " + issuer[0]._);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
throw new Error("Missing SAML issuer");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async processValidlySignedAssertionAsync(xml, samlResponseXml, inResponseTo) {
|
|
707
|
+
let msg;
|
|
708
|
+
const nowMs = new Date().getTime();
|
|
709
|
+
const profile = {};
|
|
710
|
+
const doc = await (0, xml_1.parseXml2JsFromString)(xml);
|
|
711
|
+
const parsedAssertion = doc;
|
|
712
|
+
const assertion = doc.Assertion;
|
|
713
|
+
getInResponseTo: {
|
|
714
|
+
const issuer = assertion.Issuer;
|
|
715
|
+
if (issuer && issuer[0]._) {
|
|
716
|
+
profile.issuer = issuer[0]._;
|
|
717
|
+
}
|
|
718
|
+
if (inResponseTo != null) {
|
|
719
|
+
profile.inResponseTo = inResponseTo;
|
|
720
|
+
}
|
|
721
|
+
const authnStatement = assertion.AuthnStatement;
|
|
722
|
+
if (authnStatement) {
|
|
723
|
+
if (authnStatement[0].$ && authnStatement[0].$.SessionIndex) {
|
|
724
|
+
profile.sessionIndex = authnStatement[0].$.SessionIndex;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const subject = assertion.Subject;
|
|
728
|
+
let subjectConfirmation;
|
|
729
|
+
let confirmData = null;
|
|
730
|
+
let subjectConfirmations = null;
|
|
731
|
+
if (subject) {
|
|
732
|
+
const nameID = subject[0].NameID;
|
|
733
|
+
if (nameID && nameID[0]._) {
|
|
734
|
+
profile.nameID = nameID[0]._;
|
|
735
|
+
if (nameID[0].$ && nameID[0].$.Format) {
|
|
736
|
+
profile.nameIDFormat = nameID[0].$.Format;
|
|
737
|
+
profile.nameQualifier = nameID[0].$.NameQualifier;
|
|
738
|
+
profile.spNameQualifier = nameID[0].$.SPNameQualifier;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
subjectConfirmations = subject[0].SubjectConfirmation;
|
|
742
|
+
subjectConfirmation = subjectConfirmations === null || subjectConfirmations === void 0 ? void 0 : subjectConfirmations.find((_subjectConfirmation) => {
|
|
743
|
+
var _a;
|
|
744
|
+
const _confirmData = (_a = _subjectConfirmation.SubjectConfirmationData) === null || _a === void 0 ? void 0 : _a[0];
|
|
745
|
+
if (_confirmData === null || _confirmData === void 0 ? void 0 : _confirmData.$) {
|
|
746
|
+
const subjectNotBefore = _confirmData.$.NotBefore;
|
|
747
|
+
const subjectNotOnOrAfter = _confirmData.$.NotOnOrAfter;
|
|
748
|
+
const maxTimeLimitMs = this.calcMaxAgeAssertionTime(this.options.maxAssertionAgeMs, subjectNotOnOrAfter, assertion.$.IssueInstant);
|
|
749
|
+
const subjErr = this.checkTimestampsValidityError(nowMs, subjectNotBefore, subjectNotOnOrAfter, maxTimeLimitMs);
|
|
750
|
+
if (subjErr === null)
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
return false;
|
|
754
|
+
});
|
|
755
|
+
if (subjectConfirmation != null) {
|
|
756
|
+
confirmData = subjectConfirmation.SubjectConfirmationData[0];
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Test to see that if we have a SubjectConfirmation InResponseTo that it matches
|
|
761
|
+
* the 'InResponseTo' attribute set in the Response
|
|
762
|
+
*/
|
|
763
|
+
if (this.mustValidateInResponseTo(Boolean(inResponseTo))) {
|
|
764
|
+
if (subjectConfirmation) {
|
|
765
|
+
if (confirmData === null || confirmData === void 0 ? void 0 : confirmData.$) {
|
|
766
|
+
const subjectInResponseTo = confirmData.$.InResponseTo;
|
|
767
|
+
if (inResponseTo && subjectInResponseTo && subjectInResponseTo != inResponseTo) {
|
|
768
|
+
await this.cacheProvider.removeAsync(inResponseTo);
|
|
769
|
+
throw new Error("InResponseTo does not match subjectInResponseTo");
|
|
770
|
+
}
|
|
771
|
+
else if (subjectInResponseTo) {
|
|
772
|
+
let foundValidInResponseTo = false;
|
|
773
|
+
const result = await this.cacheProvider.getAsync(subjectInResponseTo);
|
|
774
|
+
if (result) {
|
|
775
|
+
const createdAt = new Date(result);
|
|
776
|
+
if (nowMs < createdAt.getTime() + this.options.requestIdExpirationPeriodMs)
|
|
777
|
+
foundValidInResponseTo = true;
|
|
778
|
+
}
|
|
779
|
+
await this.cacheProvider.removeAsync(inResponseTo);
|
|
780
|
+
if (!foundValidInResponseTo) {
|
|
781
|
+
throw new Error("SubjectInResponseTo is not valid");
|
|
782
|
+
}
|
|
783
|
+
break getInResponseTo;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
if (subjectConfirmations != null && subjectConfirmation == null) {
|
|
789
|
+
msg = "No valid subject confirmation found among those available in the SAML assertion";
|
|
790
|
+
throw new Error(msg);
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
await this.cacheProvider.removeAsync(inResponseTo);
|
|
794
|
+
break getInResponseTo;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
break getInResponseTo;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const conditions = assertion.Conditions ? assertion.Conditions[0] : null;
|
|
803
|
+
if (assertion.Conditions && assertion.Conditions.length > 1) {
|
|
804
|
+
msg = "Unable to process multiple conditions in SAML assertion";
|
|
805
|
+
throw new Error(msg);
|
|
806
|
+
}
|
|
807
|
+
if (conditions && conditions.$) {
|
|
808
|
+
const maxTimeLimitMs = this.calcMaxAgeAssertionTime(this.options.maxAssertionAgeMs, conditions.$.NotOnOrAfter, assertion.$.IssueInstant);
|
|
809
|
+
const conErr = this.checkTimestampsValidityError(nowMs, conditions.$.NotBefore, conditions.$.NotOnOrAfter, maxTimeLimitMs);
|
|
810
|
+
if (conErr)
|
|
811
|
+
throw conErr;
|
|
812
|
+
}
|
|
813
|
+
if (this.options.audience !== false) {
|
|
814
|
+
const audienceErr = this.checkAudienceValidityError(this.options.audience, conditions.AudienceRestriction);
|
|
815
|
+
if (audienceErr)
|
|
816
|
+
throw audienceErr;
|
|
817
|
+
}
|
|
818
|
+
const attributeStatement = assertion.AttributeStatement;
|
|
819
|
+
if (attributeStatement) {
|
|
820
|
+
const attributes = [].concat(...attributeStatement
|
|
821
|
+
.filter((attr) => Array.isArray(attr.Attribute))
|
|
822
|
+
.map((attr) => attr.Attribute));
|
|
823
|
+
const attrValueMapper = (value) => {
|
|
824
|
+
const hasChildren = Object.keys(value).some((cur) => {
|
|
825
|
+
return cur !== "_" && cur !== "$";
|
|
826
|
+
});
|
|
827
|
+
return hasChildren ? value : value._;
|
|
828
|
+
};
|
|
829
|
+
if (attributes.length > 0) {
|
|
830
|
+
const profileAttributes = {};
|
|
831
|
+
attributes.forEach((attribute) => {
|
|
832
|
+
if (!Object.prototype.hasOwnProperty.call(attribute, "AttributeValue")) {
|
|
833
|
+
// if attributes has no AttributeValue child, continue
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const name = attribute.$.Name;
|
|
837
|
+
const value = attribute.AttributeValue.length === 1
|
|
838
|
+
? attrValueMapper(attribute.AttributeValue[0])
|
|
839
|
+
: attribute.AttributeValue.map(attrValueMapper);
|
|
840
|
+
profileAttributes[name] = value;
|
|
841
|
+
/**
|
|
842
|
+
* If any property is already present in profile and is also present
|
|
843
|
+
* in attributes, then skip the one from attributes. Handle this
|
|
844
|
+
* conflict gracefully without returning any error
|
|
845
|
+
*/
|
|
846
|
+
if (Object.prototype.hasOwnProperty.call(profile, name)) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
profile[name] = value;
|
|
850
|
+
});
|
|
851
|
+
profile.attributes = profileAttributes;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (!profile.mail && profile["urn:oid:0.9.2342.19200300.100.1.3"]) {
|
|
855
|
+
/**
|
|
856
|
+
* See https://spaces.internet2.edu/display/InCFederation/Supported+Attribute+Summary
|
|
857
|
+
* for definition of attribute OIDs
|
|
858
|
+
*/
|
|
859
|
+
profile.mail = profile["urn:oid:0.9.2342.19200300.100.1.3"];
|
|
860
|
+
}
|
|
861
|
+
if (!profile.email && profile.mail) {
|
|
862
|
+
profile.email = profile.mail;
|
|
863
|
+
}
|
|
864
|
+
profile.getAssertionXml = () => xml.toString();
|
|
865
|
+
profile.getAssertion = () => parsedAssertion;
|
|
866
|
+
profile.getSamlResponseXml = () => samlResponseXml;
|
|
867
|
+
return { profile, loggedOut: false };
|
|
868
|
+
}
|
|
869
|
+
checkTimestampsValidityError(nowMs, notBefore, notOnOrAfter, maxTimeLimitMs) {
|
|
870
|
+
if (this.options.acceptedClockSkewMs == -1)
|
|
871
|
+
return null;
|
|
872
|
+
if (notBefore) {
|
|
873
|
+
const notBeforeMs = (0, datetime_1.dateStringToTimestamp)(notBefore, "NotBefore");
|
|
874
|
+
if (nowMs + this.options.acceptedClockSkewMs < notBeforeMs)
|
|
875
|
+
return new Error("SAML assertion not yet valid");
|
|
876
|
+
}
|
|
877
|
+
if (notOnOrAfter) {
|
|
878
|
+
const notOnOrAfterMs = (0, datetime_1.dateStringToTimestamp)(notOnOrAfter, "NotOnOrAfter");
|
|
879
|
+
if (nowMs - this.options.acceptedClockSkewMs >= notOnOrAfterMs)
|
|
880
|
+
return new Error("SAML assertion expired: clocks skewed too much");
|
|
881
|
+
}
|
|
882
|
+
if (maxTimeLimitMs) {
|
|
883
|
+
if (nowMs - this.options.acceptedClockSkewMs >= maxTimeLimitMs)
|
|
884
|
+
return new Error("SAML assertion expired: assertion too old");
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
checkAudienceValidityError(expectedAudience, audienceRestrictions) {
|
|
889
|
+
if (!audienceRestrictions || audienceRestrictions.length < 1) {
|
|
890
|
+
return new Error("SAML assertion has no AudienceRestriction");
|
|
891
|
+
}
|
|
892
|
+
const errors = audienceRestrictions
|
|
893
|
+
.map((restriction) => {
|
|
894
|
+
if (!restriction.Audience || !restriction.Audience[0] || !restriction.Audience[0]._) {
|
|
895
|
+
return new Error("SAML assertion AudienceRestriction has no Audience value");
|
|
896
|
+
}
|
|
897
|
+
if (restriction.Audience[0]._ !== expectedAudience) {
|
|
898
|
+
return new Error("SAML assertion audience mismatch");
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
901
|
+
})
|
|
902
|
+
.filter((result) => {
|
|
903
|
+
return result !== null;
|
|
904
|
+
});
|
|
905
|
+
if (errors.length > 0) {
|
|
906
|
+
return errors[0];
|
|
907
|
+
}
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
async validatePostRequestAsync(container) {
|
|
911
|
+
const xml = Buffer.from(container.SAMLRequest, "base64").toString("utf8");
|
|
912
|
+
const dom = await (0, xml_1.parseDomFromString)(xml);
|
|
913
|
+
const doc = await (0, xml_1.parseXml2JsFromString)(xml);
|
|
914
|
+
const certs = await this.certsToCheck();
|
|
915
|
+
if (!(0, xml_1.validateSignature)(xml, dom.documentElement, certs)) {
|
|
916
|
+
throw new Error("Invalid signature on documentElement");
|
|
917
|
+
}
|
|
918
|
+
return await this.processValidlySignedPostRequestAsync(doc, dom);
|
|
919
|
+
}
|
|
920
|
+
async processValidlySignedPostRequestAsync(doc, dom) {
|
|
921
|
+
var _a;
|
|
922
|
+
const request = doc.LogoutRequest;
|
|
923
|
+
this.verifyLogoutRequest(doc);
|
|
924
|
+
if (request) {
|
|
925
|
+
const profile = {};
|
|
926
|
+
if (request.$.ID) {
|
|
927
|
+
profile.ID = request.$.ID;
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
throw new Error("Missing SAML LogoutRequest ID");
|
|
931
|
+
}
|
|
932
|
+
const issuer = request.Issuer;
|
|
933
|
+
if (issuer && issuer[0]._) {
|
|
934
|
+
profile.issuer = issuer[0]._;
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
throw new Error("Missing SAML issuer");
|
|
938
|
+
}
|
|
939
|
+
const nameID = await (0, xml_1.getNameIdAsync)(dom, (_a = this.options.decryptionPvk) !== null && _a !== void 0 ? _a : null);
|
|
940
|
+
if (nameID.value) {
|
|
941
|
+
profile.nameID = nameID.value;
|
|
942
|
+
if (nameID.format) {
|
|
943
|
+
profile.nameIDFormat = nameID.format;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
throw new Error("Missing SAML NameID");
|
|
948
|
+
}
|
|
949
|
+
const sessionIndex = request.SessionIndex;
|
|
950
|
+
if (sessionIndex) {
|
|
951
|
+
profile.sessionIndex = sessionIndex[0]._;
|
|
952
|
+
}
|
|
953
|
+
return { profile, loggedOut: true };
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
throw new Error("Unknown SAML request message");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
async processValidlySignedSamlLogoutAsync(doc, dom) {
|
|
960
|
+
const response = doc.LogoutResponse;
|
|
961
|
+
const request = doc.LogoutRequest;
|
|
962
|
+
if (response) {
|
|
963
|
+
return { profile: null, loggedOut: true };
|
|
964
|
+
}
|
|
965
|
+
else if (request) {
|
|
966
|
+
return await this.processValidlySignedPostRequestAsync(doc, dom);
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
throw new Error("Unknown SAML response message");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
generateServiceProviderMetadata(decryptionCert, signingCerts) {
|
|
973
|
+
const callbackUrl = this.getCallbackUrl(); // TODO it would probably be useful to have a host parameter here
|
|
974
|
+
return (0, metadata_1.generateServiceProviderMetadata)({
|
|
975
|
+
...this.options,
|
|
976
|
+
callbackUrl,
|
|
977
|
+
decryptionCert,
|
|
978
|
+
signingCerts,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Process max age assertion and use it if it is more restrictive than the NotOnOrAfter age
|
|
983
|
+
* assertion received in the SAMLResponse.
|
|
984
|
+
*
|
|
985
|
+
* @param maxAssertionAgeMs Max time after IssueInstant that we will accept assertion, in Ms.
|
|
986
|
+
* @param notOnOrAfter Expiration provided in response.
|
|
987
|
+
* @param issueInstant Time when response was issued.
|
|
988
|
+
* @returns {*} The expiration time to be used, in Ms.
|
|
989
|
+
*/
|
|
990
|
+
calcMaxAgeAssertionTime(maxAssertionAgeMs, notOnOrAfter, issueInstant) {
|
|
991
|
+
const notOnOrAfterMs = (0, datetime_1.dateStringToTimestamp)(notOnOrAfter, "NotOnOrAfter");
|
|
992
|
+
const issueInstantMs = (0, datetime_1.dateStringToTimestamp)(issueInstant, "IssueInstant");
|
|
993
|
+
if (maxAssertionAgeMs === 0) {
|
|
994
|
+
return notOnOrAfterMs;
|
|
995
|
+
}
|
|
996
|
+
const maxAssertionTimeMs = issueInstantMs + maxAssertionAgeMs;
|
|
997
|
+
return maxAssertionTimeMs < notOnOrAfterMs ? maxAssertionTimeMs : notOnOrAfterMs;
|
|
998
|
+
}
|
|
999
|
+
mustValidateInResponseTo(hasInResponseTo) {
|
|
1000
|
+
return (this.options.validateInResponseTo === types_1.ValidateInResponseTo.always ||
|
|
1001
|
+
(this.options.validateInResponseTo === types_1.ValidateInResponseTo.ifPresent && hasInResponseTo));
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
exports.SAML = SAML;
|
|
1005
|
+
//# sourceMappingURL=saml.js.map
|