1076 lines
No EOL
50 KiB
JavaScript
1076 lines
No EOL
50 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.SAML = void 0;
|
|
const debug_1 = require("debug");
|
|
const debug = debug_1.default("node-saml");
|
|
const zlib = require("zlib");
|
|
const crypto = require("crypto");
|
|
const url_1 = require("url");
|
|
const querystring = require("querystring");
|
|
const util = require("util");
|
|
const inmemory_cache_provider_1 = require("./inmemory-cache-provider");
|
|
const algorithms = require("./algorithms");
|
|
const saml_post_signing_1 = require("./saml-post-signing");
|
|
const types_1 = require("./types");
|
|
const types_2 = require("../passport-saml/types");
|
|
const utility_1 = require("./utility");
|
|
const xml_1 = require("./xml");
|
|
const inflateRawAsync = util.promisify(zlib.inflateRaw);
|
|
const deflateRawAsync = util.promisify(zlib.deflateRaw);
|
|
async function processValidlySignedPostRequestAsync(self, doc, dom) {
|
|
const request = doc.LogoutRequest;
|
|
if (request) {
|
|
const profile = {};
|
|
if (request.$.ID) {
|
|
profile.ID = request.$.ID;
|
|
}
|
|
else {
|
|
throw new Error("Missing SAML LogoutRequest ID");
|
|
}
|
|
const issuer = request.Issuer;
|
|
if (issuer && issuer[0]._) {
|
|
profile.issuer = issuer[0]._;
|
|
}
|
|
else {
|
|
throw new Error("Missing SAML issuer");
|
|
}
|
|
const nameID = await self._getNameIdAsync(self, dom);
|
|
if (nameID) {
|
|
profile.nameID = nameID.value;
|
|
if (nameID.format) {
|
|
profile.nameIDFormat = nameID.format;
|
|
}
|
|
}
|
|
else {
|
|
throw new Error("Missing SAML NameID");
|
|
}
|
|
const sessionIndex = request.SessionIndex;
|
|
if (sessionIndex) {
|
|
profile.sessionIndex = sessionIndex[0]._;
|
|
}
|
|
return { profile, loggedOut: true };
|
|
}
|
|
else {
|
|
throw new Error("Unknown SAML request message");
|
|
}
|
|
}
|
|
async function processValidlySignedSamlLogoutAsync(self, doc, dom) {
|
|
const response = doc.LogoutResponse;
|
|
const request = doc.LogoutRequest;
|
|
if (response) {
|
|
return { profile: null, loggedOut: true };
|
|
}
|
|
else if (request) {
|
|
return await processValidlySignedPostRequestAsync(self, doc, dom);
|
|
}
|
|
else {
|
|
throw new Error("Unknown SAML response message");
|
|
}
|
|
}
|
|
async function promiseWithNameID(nameid) {
|
|
const format = xml_1.xpath.selectAttributes(nameid, "@Format");
|
|
return {
|
|
value: nameid.textContent,
|
|
format: format && format[0] && format[0].nodeValue,
|
|
};
|
|
}
|
|
class SAML {
|
|
constructor(ctorOptions) {
|
|
this.options = this.initialize(ctorOptions);
|
|
this.cacheProvider = this.options.cacheProvider;
|
|
}
|
|
initialize(ctorOptions) {
|
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
if (!ctorOptions) {
|
|
throw new TypeError("SamlOptions required on construction");
|
|
}
|
|
const options = {
|
|
...ctorOptions,
|
|
passive: (_a = ctorOptions.passive) !== null && _a !== void 0 ? _a : false,
|
|
disableRequestedAuthnContext: (_b = ctorOptions.disableRequestedAuthnContext) !== null && _b !== void 0 ? _b : false,
|
|
additionalParams: (_c = ctorOptions.additionalParams) !== null && _c !== void 0 ? _c : {},
|
|
additionalAuthorizeParams: (_d = ctorOptions.additionalAuthorizeParams) !== null && _d !== void 0 ? _d : {},
|
|
additionalLogoutParams: (_e = ctorOptions.additionalLogoutParams) !== null && _e !== void 0 ? _e : {},
|
|
forceAuthn: (_f = ctorOptions.forceAuthn) !== null && _f !== void 0 ? _f : false,
|
|
skipRequestCompression: (_g = ctorOptions.skipRequestCompression) !== null && _g !== void 0 ? _g : false,
|
|
disableRequestAcsUrl: (_h = ctorOptions.disableRequestAcsUrl) !== null && _h !== void 0 ? _h : false,
|
|
acceptedClockSkewMs: (_j = ctorOptions.acceptedClockSkewMs) !== null && _j !== void 0 ? _j : 0,
|
|
path: (_k = ctorOptions.path) !== null && _k !== void 0 ? _k : "/saml/consume",
|
|
host: (_l = ctorOptions.host) !== null && _l !== void 0 ? _l : "localhost",
|
|
issuer: (_m = ctorOptions.issuer) !== null && _m !== void 0 ? _m : "onelogin_saml",
|
|
identifierFormat: ctorOptions.identifierFormat === undefined
|
|
? "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
|
: ctorOptions.identifierFormat,
|
|
wantAssertionsSigned: (_o = ctorOptions.wantAssertionsSigned) !== null && _o !== void 0 ? _o : false,
|
|
authnContext: (_p = ctorOptions.authnContext) !== null && _p !== void 0 ? _p : [
|
|
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
|
|
],
|
|
validateInResponseTo: (_q = ctorOptions.validateInResponseTo) !== null && _q !== void 0 ? _q : false,
|
|
cert: utility_1.assertRequired(ctorOptions.cert, "cert is required"),
|
|
requestIdExpirationPeriodMs: (_r = ctorOptions.requestIdExpirationPeriodMs) !== null && _r !== void 0 ? _r : 28800000,
|
|
cacheProvider: (_s = ctorOptions.cacheProvider) !== null && _s !== void 0 ? _s : new inmemory_cache_provider_1.CacheProvider({
|
|
keyExpirationPeriodMs: ctorOptions.requestIdExpirationPeriodMs,
|
|
}),
|
|
logoutUrl: (_u = (_t = ctorOptions.logoutUrl) !== null && _t !== void 0 ? _t : ctorOptions.entryPoint) !== null && _u !== void 0 ? _u : "",
|
|
signatureAlgorithm: (_v = ctorOptions.signatureAlgorithm) !== null && _v !== void 0 ? _v : "sha1",
|
|
authnRequestBinding: (_w = ctorOptions.authnRequestBinding) !== null && _w !== void 0 ? _w : "HTTP-Redirect",
|
|
racComparison: (_x = ctorOptions.racComparison) !== null && _x !== void 0 ? _x : "exact",
|
|
privateKey: utility_1.assertRequired(ctorOptions.privateKey),
|
|
};
|
|
/**
|
|
* List of possible values:
|
|
* - exact : Assertion context must exactly match a context in the list
|
|
* - minimum: Assertion context must be at least as strong as a context in the list
|
|
* - maximum: Assertion context must be no stronger than a context in the list
|
|
* - better: Assertion context must be stronger than all contexts in the list
|
|
*/
|
|
if (!["exact", "minimum", "maximum", "better"].includes(options.racComparison)) {
|
|
throw new TypeError("racComparison must be one of ['exact', 'minimum', 'maximum', 'better']");
|
|
}
|
|
return options;
|
|
}
|
|
getCallbackUrl(host) {
|
|
// Post-auth destination
|
|
if (this.options.callbackUrl) {
|
|
return this.options.callbackUrl;
|
|
}
|
|
else {
|
|
const url = new url_1.URL("http://localhost");
|
|
if (host) {
|
|
url.host = host;
|
|
}
|
|
else {
|
|
url.host = this.options.host;
|
|
}
|
|
if (this.options.protocol) {
|
|
url.protocol = this.options.protocol;
|
|
}
|
|
url.pathname = this.options.path;
|
|
return url.toString();
|
|
}
|
|
}
|
|
_generateUniqueID() {
|
|
return crypto.randomBytes(10).toString("hex");
|
|
}
|
|
generateInstant() {
|
|
return new Date().toISOString();
|
|
}
|
|
signRequest(samlMessage) {
|
|
this.options.privateKey = utility_1.assertRequired(this.options.privateKey, "privateKey is required");
|
|
const samlMessageToSign = {};
|
|
samlMessage.SigAlg = algorithms.getSigningAlgorithm(this.options.signatureAlgorithm);
|
|
const signer = algorithms.getSigner(this.options.signatureAlgorithm);
|
|
if (samlMessage.SAMLRequest) {
|
|
samlMessageToSign.SAMLRequest = samlMessage.SAMLRequest;
|
|
}
|
|
if (samlMessage.SAMLResponse) {
|
|
samlMessageToSign.SAMLResponse = samlMessage.SAMLResponse;
|
|
}
|
|
if (samlMessage.RelayState) {
|
|
samlMessageToSign.RelayState = samlMessage.RelayState;
|
|
}
|
|
if (samlMessage.SigAlg) {
|
|
samlMessageToSign.SigAlg = samlMessage.SigAlg;
|
|
}
|
|
signer.update(querystring.stringify(samlMessageToSign));
|
|
samlMessage.Signature = signer.sign(this._keyToPEM(this.options.privateKey), "base64");
|
|
}
|
|
async generateAuthorizeRequestAsync(isPassive, isHttpPostBinding, host) {
|
|
this.options.entryPoint = utility_1.assertRequired(this.options.entryPoint, "entryPoint is required");
|
|
const id = "_" + this._generateUniqueID();
|
|
const instant = this.generateInstant();
|
|
if (this.options.validateInResponseTo) {
|
|
await this.cacheProvider.saveAsync(id, instant);
|
|
}
|
|
const request = {
|
|
"samlp:AuthnRequest": {
|
|
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
"@ID": id,
|
|
"@Version": "2.0",
|
|
"@IssueInstant": instant,
|
|
"@ProtocolBinding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
"@Destination": this.options.entryPoint,
|
|
"saml:Issuer": {
|
|
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
"#text": this.options.issuer,
|
|
},
|
|
},
|
|
};
|
|
if (isPassive)
|
|
request["samlp:AuthnRequest"]["@IsPassive"] = true;
|
|
if (this.options.forceAuthn) {
|
|
request["samlp:AuthnRequest"]["@ForceAuthn"] = true;
|
|
}
|
|
if (!this.options.disableRequestAcsUrl) {
|
|
request["samlp:AuthnRequest"]["@AssertionConsumerServiceURL"] = this.getCallbackUrl(host);
|
|
}
|
|
if (this.options.identifierFormat != null) {
|
|
request["samlp:AuthnRequest"]["samlp:NameIDPolicy"] = {
|
|
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
"@Format": this.options.identifierFormat,
|
|
"@AllowCreate": "true",
|
|
};
|
|
}
|
|
if (!this.options.disableRequestedAuthnContext) {
|
|
const authnContextClassRefs = [];
|
|
this.options.authnContext.forEach(function (value) {
|
|
authnContextClassRefs.push({
|
|
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
"#text": value,
|
|
});
|
|
});
|
|
request["samlp:AuthnRequest"]["samlp:RequestedAuthnContext"] = {
|
|
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
"@Comparison": this.options.racComparison,
|
|
"saml:AuthnContextClassRef": authnContextClassRefs,
|
|
};
|
|
}
|
|
if (this.options.attributeConsumingServiceIndex != null) {
|
|
request["samlp:AuthnRequest"]["@AttributeConsumingServiceIndex"] = this.options.attributeConsumingServiceIndex;
|
|
}
|
|
if (this.options.providerName != null) {
|
|
request["samlp:AuthnRequest"]["@ProviderName"] = this.options.providerName;
|
|
}
|
|
if (this.options.scoping != null) {
|
|
const scoping = {
|
|
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
};
|
|
if (typeof this.options.scoping.proxyCount === "number") {
|
|
scoping["@ProxyCount"] = this.options.scoping.proxyCount;
|
|
}
|
|
if (this.options.scoping.idpList) {
|
|
scoping["samlp:IDPList"] = this.options.scoping.idpList.map((idpListItem) => {
|
|
const formattedIdpListItem = {
|
|
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
};
|
|
if (idpListItem.entries) {
|
|
formattedIdpListItem["samlp:IDPEntry"] = idpListItem.entries.map((entry) => {
|
|
const formattedEntry = {
|
|
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
};
|
|
formattedEntry["@ProviderID"] = entry.providerId;
|
|
if (entry.name) {
|
|
formattedEntry["@Name"] = entry.name;
|
|
}
|
|
if (entry.loc) {
|
|
formattedEntry["@Loc"] = entry.loc;
|
|
}
|
|
return formattedEntry;
|
|
});
|
|
}
|
|
if (idpListItem.getComplete) {
|
|
formattedIdpListItem["samlp:GetComplete"] = idpListItem.getComplete;
|
|
}
|
|
return formattedIdpListItem;
|
|
});
|
|
}
|
|
if (this.options.scoping.requesterId) {
|
|
scoping["samlp:RequesterID"] = this.options.scoping.requesterId;
|
|
}
|
|
request["samlp:AuthnRequest"]["samlp:Scoping"] = scoping;
|
|
}
|
|
let stringRequest = xml_1.buildXmlBuilderObject(request, false);
|
|
// TODO: maybe we should always sign here
|
|
if (isHttpPostBinding && types_1.isValidSamlSigningOptions(this.options)) {
|
|
stringRequest = saml_post_signing_1.signAuthnRequestPost(stringRequest, this.options);
|
|
}
|
|
return stringRequest;
|
|
}
|
|
async _generateLogoutRequest(user) {
|
|
const id = "_" + this._generateUniqueID();
|
|
const instant = this.generateInstant();
|
|
const request = {
|
|
"samlp:LogoutRequest": {
|
|
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
"@ID": id,
|
|
"@Version": "2.0",
|
|
"@IssueInstant": instant,
|
|
"@Destination": this.options.logoutUrl,
|
|
"saml:Issuer": {
|
|
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
"#text": this.options.issuer,
|
|
},
|
|
"saml:NameID": {
|
|
"@Format": user.nameIDFormat,
|
|
"#text": user.nameID,
|
|
},
|
|
},
|
|
};
|
|
if (user.nameQualifier != null) {
|
|
request["samlp:LogoutRequest"]["saml:NameID"]["@NameQualifier"] = user.nameQualifier;
|
|
}
|
|
if (user.spNameQualifier != null) {
|
|
request["samlp:LogoutRequest"]["saml:NameID"]["@SPNameQualifier"] = user.spNameQualifier;
|
|
}
|
|
if (user.sessionIndex) {
|
|
request["samlp:LogoutRequest"]["saml2p:SessionIndex"] = {
|
|
"@xmlns:saml2p": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
"#text": user.sessionIndex,
|
|
};
|
|
}
|
|
await this.cacheProvider.saveAsync(id, instant);
|
|
return xml_1.buildXmlBuilderObject(request, false);
|
|
}
|
|
_generateLogoutResponse(logoutRequest) {
|
|
const id = "_" + this._generateUniqueID();
|
|
const instant = this.generateInstant();
|
|
const request = {
|
|
"samlp:LogoutResponse": {
|
|
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
"@ID": id,
|
|
"@Version": "2.0",
|
|
"@IssueInstant": instant,
|
|
"@Destination": this.options.logoutUrl,
|
|
"@InResponseTo": logoutRequest.ID,
|
|
"saml:Issuer": {
|
|
"#text": this.options.issuer,
|
|
},
|
|
"samlp:Status": {
|
|
"samlp:StatusCode": {
|
|
"@Value": "urn:oasis:names:tc:SAML:2.0:status:Success",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
return xml_1.buildXmlBuilderObject(request, false);
|
|
}
|
|
async _requestToUrlAsync(request, response, operation, additionalParameters) {
|
|
this.options.entryPoint = utility_1.assertRequired(this.options.entryPoint, "entryPoint is required");
|
|
let buffer;
|
|
if (this.options.skipRequestCompression) {
|
|
buffer = Buffer.from((request || response), "utf8");
|
|
}
|
|
else {
|
|
buffer = await deflateRawAsync((request || response));
|
|
}
|
|
const base64 = buffer.toString("base64");
|
|
let target = new url_1.URL(this.options.entryPoint);
|
|
if (operation === "logout") {
|
|
if (this.options.logoutUrl) {
|
|
target = new url_1.URL(this.options.logoutUrl);
|
|
}
|
|
}
|
|
else if (operation !== "authorize") {
|
|
throw new Error("Unknown operation: " + operation);
|
|
}
|
|
const samlMessage = request
|
|
? {
|
|
SAMLRequest: base64,
|
|
}
|
|
: {
|
|
SAMLResponse: base64,
|
|
};
|
|
Object.keys(additionalParameters).forEach((k) => {
|
|
samlMessage[k] = additionalParameters[k];
|
|
});
|
|
if (this.options.privateKey != null) {
|
|
if (!this.options.entryPoint) {
|
|
throw new Error('"entryPoint" config parameter is required for signed messages');
|
|
}
|
|
// sets .SigAlg and .Signature
|
|
this.signRequest(samlMessage);
|
|
}
|
|
Object.keys(samlMessage).forEach((k) => {
|
|
target.searchParams.set(k, samlMessage[k]);
|
|
});
|
|
return target.toString();
|
|
}
|
|
_getAdditionalParams(RelayState, operation, overrideParams) {
|
|
const additionalParams = {};
|
|
if (typeof RelayState === "string" && RelayState.length > 0) {
|
|
additionalParams.RelayState = RelayState;
|
|
}
|
|
const optionsAdditionalParams = this.options.additionalParams;
|
|
Object.keys(optionsAdditionalParams).forEach(function (k) {
|
|
additionalParams[k] = optionsAdditionalParams[k];
|
|
});
|
|
let optionsAdditionalParamsForThisOperation = {};
|
|
if (operation == "authorize") {
|
|
optionsAdditionalParamsForThisOperation = this.options.additionalAuthorizeParams;
|
|
}
|
|
if (operation == "logout") {
|
|
optionsAdditionalParamsForThisOperation = this.options.additionalLogoutParams;
|
|
}
|
|
Object.keys(optionsAdditionalParamsForThisOperation).forEach(function (k) {
|
|
additionalParams[k] = optionsAdditionalParamsForThisOperation[k];
|
|
});
|
|
overrideParams = overrideParams !== null && overrideParams !== void 0 ? overrideParams : {};
|
|
Object.keys(overrideParams).forEach(function (k) {
|
|
additionalParams[k] = overrideParams[k];
|
|
});
|
|
return additionalParams;
|
|
}
|
|
async getAuthorizeUrlAsync(RelayState, host, options) {
|
|
const request = await this.generateAuthorizeRequestAsync(this.options.passive, false, host);
|
|
const operation = "authorize";
|
|
const overrideParams = options ? options.additionalParams || {} : {};
|
|
return await this._requestToUrlAsync(request, null, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
}
|
|
async getAuthorizeFormAsync(RelayState, host) {
|
|
this.options.entryPoint = utility_1.assertRequired(this.options.entryPoint, "entryPoint is required");
|
|
// The quoteattr() function is used in a context, where the result will not be evaluated by javascript
|
|
// but must be interpreted by an XML or HTML parser, and it must absolutely avoid breaking the syntax
|
|
// of an element attribute.
|
|
const quoteattr = function (s, preserveCR) {
|
|
const preserveCRChar = preserveCR ? " " : "\n";
|
|
return (("" + s) // Forces the conversion to string.
|
|
.replace(/&/g, "&") // This MUST be the 1st replacement.
|
|
.replace(/'/g, "'") // The 4 other predefined entities, required.
|
|
.replace(/"/g, """)
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
// Add other replacements here for HTML only
|
|
// Or for XML, only if the named entities are defined in its DTD.
|
|
.replace(/\r\n/g, preserveCRChar) // Must be before the next replacement.
|
|
.replace(/[\r\n]/g, preserveCRChar));
|
|
};
|
|
const request = await this.generateAuthorizeRequestAsync(this.options.passive, true, host);
|
|
let buffer;
|
|
if (this.options.skipRequestCompression) {
|
|
buffer = Buffer.from(request, "utf8");
|
|
}
|
|
else {
|
|
buffer = await deflateRawAsync(request);
|
|
}
|
|
const operation = "authorize";
|
|
const additionalParameters = this._getAdditionalParams(RelayState, operation);
|
|
const samlMessage = {
|
|
SAMLRequest: buffer.toString("base64"),
|
|
};
|
|
Object.keys(additionalParameters).forEach((k) => {
|
|
samlMessage[k] = additionalParameters[k] || "";
|
|
});
|
|
const formInputs = Object.keys(samlMessage)
|
|
.map((k) => {
|
|
return '<input type="hidden" name="' + k + '" value="' + quoteattr(samlMessage[k]) + '" />';
|
|
})
|
|
.join("\r\n");
|
|
return [
|
|
"<!DOCTYPE html>",
|
|
"<html>",
|
|
"<head>",
|
|
'<meta charset="utf-8">',
|
|
'<meta http-equiv="x-ua-compatible" content="ie=edge">',
|
|
"</head>",
|
|
'<body onload="document.forms[0].submit()">',
|
|
"<noscript>",
|
|
"<p><strong>Note:</strong> Since your browser does not support JavaScript, you must press the button below once to proceed.</p>",
|
|
"</noscript>",
|
|
'<form method="post" action="' + encodeURI(this.options.entryPoint) + '">',
|
|
formInputs,
|
|
'<input type="submit" value="Submit" />',
|
|
"</form>",
|
|
'<script>document.forms[0].style.display="none";</script>',
|
|
"</body>",
|
|
"</html>",
|
|
].join("\r\n");
|
|
}
|
|
async getLogoutUrlAsync(user, RelayState, options) {
|
|
const request = await this._generateLogoutRequest(user);
|
|
const operation = "logout";
|
|
const overrideParams = options ? options.additionalParams || {} : {};
|
|
return await this._requestToUrlAsync(request, null, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
}
|
|
getLogoutResponseUrl(samlLogoutRequest, RelayState, options, callback) {
|
|
util.callbackify(() => this.getLogoutResponseUrlAsync(samlLogoutRequest, RelayState, options))(callback);
|
|
}
|
|
async getLogoutResponseUrlAsync(samlLogoutRequest, RelayState, options // add RelayState,
|
|
) {
|
|
const response = this._generateLogoutResponse(samlLogoutRequest);
|
|
const operation = "logout";
|
|
const overrideParams = options ? options.additionalParams || {} : {};
|
|
return await this._requestToUrlAsync(null, response, operation, this._getAdditionalParams(RelayState, operation, overrideParams));
|
|
}
|
|
_certToPEM(cert) {
|
|
cert = cert.match(/.{1,64}/g).join("\n");
|
|
if (cert.indexOf("-BEGIN CERTIFICATE-") === -1)
|
|
cert = "-----BEGIN CERTIFICATE-----\n" + cert;
|
|
if (cert.indexOf("-END CERTIFICATE-") === -1)
|
|
cert = cert + "\n-----END CERTIFICATE-----\n";
|
|
return cert;
|
|
}
|
|
async certsToCheck() {
|
|
let checkedCerts;
|
|
if (typeof this.options.cert === "function") {
|
|
checkedCerts = await util
|
|
.promisify(this.options.cert)()
|
|
.then((certs) => {
|
|
certs = utility_1.assertRequired(certs, "callback didn't return cert");
|
|
if (!Array.isArray(certs)) {
|
|
certs = [certs];
|
|
}
|
|
return certs;
|
|
});
|
|
}
|
|
else if (Array.isArray(this.options.cert)) {
|
|
checkedCerts = this.options.cert;
|
|
}
|
|
else {
|
|
checkedCerts = [this.options.cert];
|
|
}
|
|
checkedCerts.forEach((cert) => {
|
|
utility_1.assertRequired(cert, "unknown cert found");
|
|
});
|
|
return checkedCerts;
|
|
}
|
|
// This function checks that the |currentNode| in the |fullXml| document contains exactly 1 valid
|
|
// signature of the |currentNode|.
|
|
//
|
|
// See https://github.com/bergie/passport-saml/issues/19 for references to some of the attack
|
|
// vectors against SAML signature verification.
|
|
validateSignature(fullXml, currentNode, certs) {
|
|
const xpathSigQuery = ".//*[" +
|
|
"local-name(.)='Signature' and " +
|
|
"namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#' and " +
|
|
"descendant::*[local-name(.)='Reference' and @URI='#" +
|
|
currentNode.getAttribute("ID") +
|
|
"']" +
|
|
"]";
|
|
const signatures = xml_1.xpath.selectElements(currentNode, xpathSigQuery);
|
|
// This function is expecting to validate exactly one signature, so if we find more or fewer
|
|
// than that, reject.
|
|
if (signatures.length !== 1) {
|
|
return false;
|
|
}
|
|
const signature = signatures[0];
|
|
return certs.some((certToCheck) => {
|
|
return xml_1.validateXmlSignatureForCert(signature, this._certToPEM(certToCheck), fullXml, currentNode);
|
|
});
|
|
}
|
|
async validatePostResponseAsync(container) {
|
|
let xml, doc, inResponseTo;
|
|
try {
|
|
xml = Buffer.from(container.SAMLResponse, "base64").toString("utf8");
|
|
doc = xml_1.parseDomFromString(xml);
|
|
if (!Object.prototype.hasOwnProperty.call(doc, "documentElement"))
|
|
throw new Error("SAMLResponse is not valid base64-encoded XML");
|
|
const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
|
|
if (inResponseToNodes) {
|
|
inResponseTo = inResponseToNodes.length ? inResponseToNodes[0].nodeValue : null;
|
|
await this.validateInResponseTo(inResponseTo);
|
|
}
|
|
const certs = await this.certsToCheck();
|
|
// Check if this document has a valid top-level signature
|
|
let validSignature = false;
|
|
if (this.validateSignature(xml, doc.documentElement, certs)) {
|
|
validSignature = true;
|
|
}
|
|
const assertions = xml_1.xpath.selectElements(doc, "/*[local-name()='Response']/*[local-name()='Assertion']");
|
|
const encryptedAssertions = xml_1.xpath.selectElements(doc, "/*[local-name()='Response']/*[local-name()='EncryptedAssertion']");
|
|
if (assertions.length + encryptedAssertions.length > 1) {
|
|
// There's no reason I know of that we want to handle multiple assertions, and it seems like a
|
|
// potential risk vector for signature scope issues, so treat this as an invalid signature
|
|
throw new Error("Invalid signature: multiple assertions");
|
|
}
|
|
if (assertions.length == 1) {
|
|
if ((this.options.wantAssertionsSigned || !validSignature) &&
|
|
!this.validateSignature(xml, assertions[0], certs)) {
|
|
throw new Error("Invalid signature");
|
|
}
|
|
return await this.processValidlySignedAssertionAsync(assertions[0].toString(), xml, inResponseTo);
|
|
}
|
|
if (encryptedAssertions.length == 1) {
|
|
this.options.decryptionPvk = utility_1.assertRequired(this.options.decryptionPvk, "No decryption key for encrypted SAML response");
|
|
const encryptedAssertionXml = encryptedAssertions[0].toString();
|
|
const decryptedXml = await xml_1.decryptXml(encryptedAssertionXml, this.options.decryptionPvk);
|
|
const decryptedDoc = xml_1.parseDomFromString(decryptedXml);
|
|
const decryptedAssertions = xml_1.xpath.selectElements(decryptedDoc, "/*[local-name()='Assertion']");
|
|
if (decryptedAssertions.length != 1)
|
|
throw new Error("Invalid EncryptedAssertion content");
|
|
if ((this.options.wantAssertionsSigned || !validSignature) &&
|
|
!this.validateSignature(decryptedXml, decryptedAssertions[0], certs)) {
|
|
throw new Error("Invalid signature from encrypted assertion");
|
|
}
|
|
return await this.processValidlySignedAssertionAsync(decryptedAssertions[0].toString(), xml, inResponseTo);
|
|
}
|
|
// If there's no assertion, fall back on xml2js response parsing for the status &
|
|
// LogoutResponse code.
|
|
const xmljsDoc = await xml_1.parseXml2JsFromString(xml);
|
|
const response = xmljsDoc.Response;
|
|
if (response) {
|
|
const assertion = response.Assertion;
|
|
if (!assertion) {
|
|
const status = response.Status;
|
|
if (status) {
|
|
const statusCode = status[0].StatusCode;
|
|
if (statusCode &&
|
|
statusCode[0].$.Value === "urn:oasis:names:tc:SAML:2.0:status:Responder") {
|
|
const nestedStatusCode = statusCode[0].StatusCode;
|
|
if (nestedStatusCode &&
|
|
nestedStatusCode[0].$.Value === "urn:oasis:names:tc:SAML:2.0:status:NoPassive") {
|
|
if (!validSignature) {
|
|
throw new Error("Invalid signature: NoPassive");
|
|
}
|
|
return { profile: null, loggedOut: false };
|
|
}
|
|
}
|
|
// Note that we're not requiring a valid signature before this logic -- since we are
|
|
// throwing an error in any case, and some providers don't sign error results,
|
|
// let's go ahead and give the potentially more helpful error.
|
|
if (statusCode && statusCode[0].$.Value) {
|
|
const msgType = statusCode[0].$.Value.match(/[^:]*$/)[0];
|
|
if (msgType != "Success") {
|
|
let msg = "unspecified";
|
|
if (status[0].StatusMessage) {
|
|
msg = status[0].StatusMessage[0]._;
|
|
}
|
|
else if (statusCode[0].StatusCode) {
|
|
msg = statusCode[0].StatusCode[0].$.Value.match(/[^:]*$/)[0];
|
|
}
|
|
const statusXml = xml_1.buildXml2JsObject("Status", status[0]);
|
|
throw new types_2.ErrorWithXmlStatus("SAML provider returned " + msgType + " error: " + msg, statusXml);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
throw new Error("Missing SAML assertion");
|
|
}
|
|
else {
|
|
if (!validSignature) {
|
|
throw new Error("Invalid signature: No response found");
|
|
}
|
|
const logoutResponse = xmljsDoc.LogoutResponse;
|
|
if (logoutResponse) {
|
|
return { profile: null, loggedOut: true };
|
|
}
|
|
else {
|
|
throw new Error("Unknown SAML response message");
|
|
}
|
|
}
|
|
}
|
|
catch (err) {
|
|
debug("validatePostResponse resulted in an error: %s", err);
|
|
if (this.options.validateInResponseTo != null) {
|
|
await this.cacheProvider.removeAsync(inResponseTo);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
async validateInResponseTo(inResponseTo) {
|
|
if (this.options.validateInResponseTo) {
|
|
if (inResponseTo) {
|
|
const result = await this.cacheProvider.getAsync(inResponseTo);
|
|
if (!result)
|
|
throw new Error("InResponseTo is not valid");
|
|
return;
|
|
}
|
|
else {
|
|
throw new Error("InResponseTo is missing from response");
|
|
}
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
async validateRedirectAsync(container, originalQuery) {
|
|
const samlMessageType = container.SAMLRequest ? "SAMLRequest" : "SAMLResponse";
|
|
const data = Buffer.from(container[samlMessageType], "base64");
|
|
const inflated = await inflateRawAsync(data);
|
|
const dom = xml_1.parseDomFromString(inflated.toString());
|
|
const doc = await xml_1.parseXml2JsFromString(inflated);
|
|
samlMessageType === "SAMLResponse"
|
|
? await this.verifyLogoutResponse(doc)
|
|
: this.verifyLogoutRequest(doc);
|
|
await this.hasValidSignatureForRedirect(container, originalQuery);
|
|
return await processValidlySignedSamlLogoutAsync(this, doc, dom);
|
|
}
|
|
async hasValidSignatureForRedirect(container, originalQuery) {
|
|
const tokens = originalQuery.split("&");
|
|
const getParam = (key) => {
|
|
const exists = tokens.filter((t) => {
|
|
return new RegExp(key).test(t);
|
|
});
|
|
return exists[0];
|
|
};
|
|
if (container.Signature) {
|
|
let urlString = getParam("SAMLRequest") || getParam("SAMLResponse");
|
|
if (getParam("RelayState")) {
|
|
urlString += "&" + getParam("RelayState");
|
|
}
|
|
urlString += "&" + getParam("SigAlg");
|
|
const certs = await this.certsToCheck();
|
|
const hasValidQuerySignature = certs.some((cert) => {
|
|
return this.validateSignatureForRedirect(urlString, container.Signature, container.SigAlg, cert);
|
|
});
|
|
if (!hasValidQuerySignature) {
|
|
throw new Error("Invalid query signature");
|
|
}
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
validateSignatureForRedirect(urlString, signature, alg, cert) {
|
|
// See if we support a matching algorithm, case-insensitive. Otherwise, throw error.
|
|
function hasMatch(ourAlgo) {
|
|
// The incoming algorithm is forwarded as a URL.
|
|
// We trim everything before the last # get something we can compare to the Node.js list
|
|
const algFromURI = alg.toLowerCase().replace(/.*#(.*)$/, "$1");
|
|
return ourAlgo.toLowerCase() === algFromURI;
|
|
}
|
|
const i = crypto.getHashes().findIndex(hasMatch);
|
|
let matchingAlgo;
|
|
if (i > -1) {
|
|
matchingAlgo = crypto.getHashes()[i];
|
|
}
|
|
else {
|
|
throw new Error(alg + " is not supported");
|
|
}
|
|
const verifier = crypto.createVerify(matchingAlgo);
|
|
verifier.update(urlString);
|
|
return verifier.verify(this._certToPEM(cert), signature, "base64");
|
|
}
|
|
verifyLogoutRequest(doc) {
|
|
this.verifyIssuer(doc.LogoutRequest);
|
|
const nowMs = new Date().getTime();
|
|
const conditions = doc.LogoutRequest.$;
|
|
const conErr = this.checkTimestampsValidityError(nowMs, conditions.NotBefore, conditions.NotOnOrAfter);
|
|
if (conErr) {
|
|
throw conErr;
|
|
}
|
|
}
|
|
async verifyLogoutResponse(doc) {
|
|
const statusCode = doc.LogoutResponse.Status[0].StatusCode[0].$.Value;
|
|
if (statusCode !== "urn:oasis:names:tc:SAML:2.0:status:Success")
|
|
throw new Error("Bad status code: " + statusCode);
|
|
this.verifyIssuer(doc.LogoutResponse);
|
|
const inResponseTo = doc.LogoutResponse.$.InResponseTo;
|
|
if (inResponseTo) {
|
|
return this.validateInResponseTo(inResponseTo);
|
|
}
|
|
return true;
|
|
}
|
|
verifyIssuer(samlMessage) {
|
|
if (this.options.idpIssuer != null) {
|
|
const issuer = samlMessage.Issuer;
|
|
if (issuer) {
|
|
if (issuer[0]._ !== this.options.idpIssuer)
|
|
throw new Error("Unknown SAML issuer. Expected: " + this.options.idpIssuer + " Received: " + issuer[0]._);
|
|
}
|
|
else {
|
|
throw new Error("Missing SAML issuer");
|
|
}
|
|
}
|
|
}
|
|
async processValidlySignedAssertionAsync(xml, samlResponseXml, inResponseTo) {
|
|
let msg;
|
|
const nowMs = new Date().getTime();
|
|
const profile = {};
|
|
const doc = await xml_1.parseXml2JsFromString(xml);
|
|
const parsedAssertion = doc;
|
|
const assertion = doc.Assertion;
|
|
getInResponseTo: {
|
|
const issuer = assertion.Issuer;
|
|
if (issuer && issuer[0]._) {
|
|
profile.issuer = issuer[0]._;
|
|
}
|
|
if (inResponseTo) {
|
|
profile.inResponseTo = inResponseTo;
|
|
}
|
|
const authnStatement = assertion.AuthnStatement;
|
|
if (authnStatement) {
|
|
if (authnStatement[0].$ && authnStatement[0].$.SessionIndex) {
|
|
profile.sessionIndex = authnStatement[0].$.SessionIndex;
|
|
}
|
|
}
|
|
const subject = assertion.Subject;
|
|
let subjectConfirmation, confirmData;
|
|
if (subject) {
|
|
const nameID = subject[0].NameID;
|
|
if (nameID && nameID[0]._) {
|
|
profile.nameID = nameID[0]._;
|
|
if (nameID[0].$ && nameID[0].$.Format) {
|
|
profile.nameIDFormat = nameID[0].$.Format;
|
|
profile.nameQualifier = nameID[0].$.NameQualifier;
|
|
profile.spNameQualifier = nameID[0].$.SPNameQualifier;
|
|
}
|
|
}
|
|
subjectConfirmation = subject[0].SubjectConfirmation
|
|
? subject[0].SubjectConfirmation[0]
|
|
: null;
|
|
confirmData =
|
|
subjectConfirmation && subjectConfirmation.SubjectConfirmationData
|
|
? subjectConfirmation.SubjectConfirmationData[0]
|
|
: null;
|
|
if (subject[0].SubjectConfirmation && subject[0].SubjectConfirmation.length > 1) {
|
|
msg = "Unable to process multiple SubjectConfirmations in SAML assertion";
|
|
throw new Error(msg);
|
|
}
|
|
if (subjectConfirmation) {
|
|
if (confirmData && confirmData.$) {
|
|
const subjectNotBefore = confirmData.$.NotBefore;
|
|
const subjectNotOnOrAfter = confirmData.$.NotOnOrAfter;
|
|
const subjErr = this.checkTimestampsValidityError(nowMs, subjectNotBefore, subjectNotOnOrAfter);
|
|
if (subjErr) {
|
|
throw subjErr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Test to see that if we have a SubjectConfirmation InResponseTo that it matches
|
|
// the 'InResponseTo' attribute set in the Response
|
|
if (this.options.validateInResponseTo) {
|
|
if (subjectConfirmation) {
|
|
if (confirmData && confirmData.$) {
|
|
const subjectInResponseTo = confirmData.$.InResponseTo;
|
|
if (inResponseTo && subjectInResponseTo && subjectInResponseTo != inResponseTo) {
|
|
await this.cacheProvider.removeAsync(inResponseTo);
|
|
throw new Error("InResponseTo is not valid");
|
|
}
|
|
else if (subjectInResponseTo) {
|
|
let foundValidInResponseTo = false;
|
|
const result = await this.cacheProvider.getAsync(subjectInResponseTo);
|
|
if (result) {
|
|
const createdAt = new Date(result);
|
|
if (nowMs < createdAt.getTime() + this.options.requestIdExpirationPeriodMs)
|
|
foundValidInResponseTo = true;
|
|
}
|
|
await this.cacheProvider.removeAsync(inResponseTo);
|
|
if (!foundValidInResponseTo) {
|
|
throw new Error("InResponseTo is not valid");
|
|
}
|
|
break getInResponseTo;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
await this.cacheProvider.removeAsync(inResponseTo);
|
|
break getInResponseTo;
|
|
}
|
|
}
|
|
else {
|
|
break getInResponseTo;
|
|
}
|
|
}
|
|
const conditions = assertion.Conditions ? assertion.Conditions[0] : null;
|
|
if (assertion.Conditions && assertion.Conditions.length > 1) {
|
|
msg = "Unable to process multiple conditions in SAML assertion";
|
|
throw new Error(msg);
|
|
}
|
|
if (conditions && conditions.$) {
|
|
const conErr = this.checkTimestampsValidityError(nowMs, conditions.$.NotBefore, conditions.$.NotOnOrAfter);
|
|
if (conErr)
|
|
throw conErr;
|
|
}
|
|
if (this.options.audience != null) {
|
|
const audienceErr = this.checkAudienceValidityError(this.options.audience, conditions.AudienceRestriction);
|
|
if (audienceErr)
|
|
throw audienceErr;
|
|
}
|
|
const attributeStatement = assertion.AttributeStatement;
|
|
if (attributeStatement) {
|
|
const attributes = [].concat(...attributeStatement
|
|
.filter((attr) => Array.isArray(attr.Attribute))
|
|
.map((attr) => attr.Attribute));
|
|
const attrValueMapper = (value) => {
|
|
const hasChildren = Object.keys(value).some((cur) => {
|
|
return cur !== "_" && cur !== "$";
|
|
});
|
|
return hasChildren ? value : value._;
|
|
};
|
|
if (attributes) {
|
|
attributes.forEach((attribute) => {
|
|
if (!Object.prototype.hasOwnProperty.call(attribute, "AttributeValue")) {
|
|
// if attributes has no AttributeValue child, continue
|
|
return;
|
|
}
|
|
const value = attribute.AttributeValue;
|
|
if (value.length === 1) {
|
|
profile[attribute.$.Name] = attrValueMapper(value[0]);
|
|
}
|
|
else {
|
|
profile[attribute.$.Name] = value.map(attrValueMapper);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (!profile.mail && profile["urn:oid:0.9.2342.19200300.100.1.3"]) {
|
|
// See https://spaces.internet2.edu/display/InCFederation/Supported+Attribute+Summary
|
|
// for definition of attribute OIDs
|
|
profile.mail = profile["urn:oid:0.9.2342.19200300.100.1.3"];
|
|
}
|
|
if (!profile.email && profile.mail) {
|
|
profile.email = profile.mail;
|
|
}
|
|
profile.getAssertionXml = () => xml.toString();
|
|
profile.getAssertion = () => parsedAssertion;
|
|
profile.getSamlResponseXml = () => samlResponseXml;
|
|
return { profile, loggedOut: false };
|
|
}
|
|
checkTimestampsValidityError(nowMs, notBefore, notOnOrAfter) {
|
|
if (this.options.acceptedClockSkewMs == -1)
|
|
return null;
|
|
if (notBefore) {
|
|
const notBeforeMs = Date.parse(notBefore);
|
|
if (nowMs + this.options.acceptedClockSkewMs < notBeforeMs)
|
|
return new Error("SAML assertion not yet valid");
|
|
}
|
|
if (notOnOrAfter) {
|
|
const notOnOrAfterMs = Date.parse(notOnOrAfter);
|
|
if (nowMs - this.options.acceptedClockSkewMs >= notOnOrAfterMs)
|
|
return new Error("SAML assertion expired");
|
|
}
|
|
return null;
|
|
}
|
|
checkAudienceValidityError(expectedAudience, audienceRestrictions) {
|
|
if (!audienceRestrictions || audienceRestrictions.length < 1) {
|
|
return new Error("SAML assertion has no AudienceRestriction");
|
|
}
|
|
const errors = audienceRestrictions
|
|
.map((restriction) => {
|
|
if (!restriction.Audience || !restriction.Audience[0] || !restriction.Audience[0]._) {
|
|
return new Error("SAML assertion AudienceRestriction has no Audience value");
|
|
}
|
|
if (restriction.Audience[0]._ !== expectedAudience) {
|
|
return new Error("SAML assertion audience mismatch");
|
|
}
|
|
return null;
|
|
})
|
|
.filter((result) => {
|
|
return result !== null;
|
|
});
|
|
if (errors.length > 0) {
|
|
return errors[0];
|
|
}
|
|
return null;
|
|
}
|
|
async validatePostRequestAsync(container) {
|
|
const xml = Buffer.from(container.SAMLRequest, "base64").toString("utf8");
|
|
const dom = xml_1.parseDomFromString(xml);
|
|
const doc = await xml_1.parseXml2JsFromString(xml);
|
|
const certs = await this.certsToCheck();
|
|
if (!this.validateSignature(xml, dom.documentElement, certs)) {
|
|
throw new Error("Invalid signature on documentElement");
|
|
}
|
|
return await processValidlySignedPostRequestAsync(this, doc, dom);
|
|
}
|
|
async _getNameIdAsync(self, doc) {
|
|
const nameIds = xml_1.xpath.selectElements(doc, "/*[local-name()='LogoutRequest']/*[local-name()='NameID']");
|
|
const encryptedIds = xml_1.xpath.selectElements(doc, "/*[local-name()='LogoutRequest']/*[local-name()='EncryptedID']");
|
|
if (nameIds.length + encryptedIds.length > 1) {
|
|
throw new Error("Invalid LogoutRequest");
|
|
}
|
|
if (nameIds.length === 1) {
|
|
return promiseWithNameID(nameIds[0]);
|
|
}
|
|
if (encryptedIds.length === 1) {
|
|
self.options.decryptionPvk = utility_1.assertRequired(self.options.decryptionPvk, "No decryption key found getting name ID for encrypted SAML response");
|
|
const encryptedDatas = xml_1.xpath.selectElements(encryptedIds[0], "./*[local-name()='EncryptedData']");
|
|
if (encryptedDatas.length !== 1) {
|
|
throw new Error("Invalid LogoutRequest");
|
|
}
|
|
const encryptedDataXml = encryptedDatas[0].toString();
|
|
const decryptedXml = await xml_1.decryptXml(encryptedDataXml, self.options.decryptionPvk);
|
|
const decryptedDoc = xml_1.parseDomFromString(decryptedXml);
|
|
const decryptedIds = xml_1.xpath.selectElements(decryptedDoc, "/*[local-name()='NameID']");
|
|
if (decryptedIds.length !== 1) {
|
|
throw new Error("Invalid EncryptedAssertion content");
|
|
}
|
|
return await promiseWithNameID(decryptedIds[0]);
|
|
}
|
|
throw new Error("Missing SAML NameID");
|
|
}
|
|
generateServiceProviderMetadata(decryptionCert, signingCert) {
|
|
const metadata = {
|
|
EntityDescriptor: {
|
|
"@xmlns": "urn:oasis:names:tc:SAML:2.0:metadata",
|
|
"@xmlns:ds": "http://www.w3.org/2000/09/xmldsig#",
|
|
"@entityID": this.options.issuer,
|
|
"@ID": this.options.issuer.replace(/\W/g, "_"),
|
|
SPSSODescriptor: {
|
|
"@protocolSupportEnumeration": "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
},
|
|
},
|
|
};
|
|
if (this.options.decryptionPvk != null) {
|
|
if (!decryptionCert) {
|
|
throw new Error("Missing decryptionCert while generating metadata for decrypting service provider");
|
|
}
|
|
}
|
|
if (this.options.privateKey != null) {
|
|
if (!signingCert) {
|
|
throw new Error("Missing signingCert while generating metadata for signing service provider messages");
|
|
}
|
|
}
|
|
if (this.options.decryptionPvk != null || this.options.privateKey != null) {
|
|
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor = [];
|
|
if (this.options.privateKey != null) {
|
|
signingCert = signingCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, "");
|
|
signingCert = signingCert.replace(/-+END CERTIFICATE-+\r?\n?/, "");
|
|
signingCert = signingCert.replace(/\r\n/g, "\n");
|
|
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor.push({
|
|
"@use": "signing",
|
|
"ds:KeyInfo": {
|
|
"ds:X509Data": {
|
|
"ds:X509Certificate": {
|
|
"#text": signingCert,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
if (this.options.decryptionPvk != null) {
|
|
decryptionCert = decryptionCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, "");
|
|
decryptionCert = decryptionCert.replace(/-+END CERTIFICATE-+\r?\n?/, "");
|
|
decryptionCert = decryptionCert.replace(/\r\n/g, "\n");
|
|
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor.push({
|
|
"@use": "encryption",
|
|
"ds:KeyInfo": {
|
|
"ds:X509Data": {
|
|
"ds:X509Certificate": {
|
|
"#text": decryptionCert,
|
|
},
|
|
},
|
|
},
|
|
EncryptionMethod: [
|
|
// this should be the set that the xmlenc library supports
|
|
{ "@Algorithm": "http://www.w3.org/2001/04/xmlenc#aes256-cbc" },
|
|
{ "@Algorithm": "http://www.w3.org/2001/04/xmlenc#aes128-cbc" },
|
|
{ "@Algorithm": "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" },
|
|
],
|
|
});
|
|
}
|
|
}
|
|
if (this.options.logoutCallbackUrl != null) {
|
|
metadata.EntityDescriptor.SPSSODescriptor.SingleLogoutService = {
|
|
"@Binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
"@Location": this.options.logoutCallbackUrl,
|
|
};
|
|
}
|
|
if (this.options.identifierFormat != null) {
|
|
metadata.EntityDescriptor.SPSSODescriptor.NameIDFormat = this.options.identifierFormat;
|
|
}
|
|
if (this.options.wantAssertionsSigned) {
|
|
metadata.EntityDescriptor.SPSSODescriptor["@WantAssertionsSigned"] = true;
|
|
}
|
|
metadata.EntityDescriptor.SPSSODescriptor.AssertionConsumerService = {
|
|
"@index": "1",
|
|
"@isDefault": "true",
|
|
"@Binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
"@Location": this.getCallbackUrl(),
|
|
};
|
|
return xml_1.buildXmlBuilderObject(metadata, true);
|
|
}
|
|
_keyToPEM(key) {
|
|
key = utility_1.assertRequired(key, "key is required");
|
|
if (typeof key !== "string")
|
|
return key;
|
|
if (key.split(/\r?\n/).length !== 1)
|
|
return key;
|
|
const matchedKey = key.match(/.{1,64}/g);
|
|
if (matchedKey) {
|
|
const wrappedKey = [
|
|
"-----BEGIN PRIVATE KEY-----",
|
|
...matchedKey,
|
|
"-----END PRIVATE KEY-----",
|
|
"",
|
|
].join("\n");
|
|
return wrappedKey;
|
|
}
|
|
throw new Error("Invalid key");
|
|
}
|
|
}
|
|
exports.SAML = SAML;
|
|
//# sourceMappingURL=saml.js.map
|