@pagopa/io-spid-commons
Version:
Common code for integrating SPID authentication
508 lines • 37.1 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertionValidation = exports.transformsValidation = exports.TransformError = exports.isEmptyNode = exports.mainAttributeValidation = exports.validateIssuer = exports.getAuthorizeRequestTamperer = exports.getMetadataTamperer = exports.getSamlOptions = exports.logSamlCertExpiration = exports.getIDFromRequest = exports.getSamlIssuer = exports.getErrorCodeFromResponse = exports.getXmlFromSamlResponse = exports.safeXMLParseFromString = exports.notSignedWithHmacPredicate = exports.extractAndLogTimings = exports.InfoNotAvailable = exports.ERROR_SAML_RESPONSE_MISSING = exports.ISSUER_FORMAT = exports.SPID_TAGS = exports.XML_TAGS = exports.SAML_NAMESPACE = void 0;
/**
* Methods used to tamper passport-saml generated SAML XML.
*
* SPID protocol has some peculiarities that need to be addressed
* to make request, metadata and responses compliant.
*/
const jose = require("jose");
const dates_1 = require("@pagopa/ts-commons/lib/dates");
const strings_1 = require("@pagopa/ts-commons/lib/strings");
const date_fns_1 = require("date-fns");
const fp_ts_1 = require("fp-ts");
const Array_1 = require("fp-ts/lib/Array");
const E = require("fp-ts/lib/Either");
const function_1 = require("fp-ts/lib/function");
const O = require("fp-ts/lib/Option");
const Record_1 = require("fp-ts/lib/Record");
const string_1 = require("fp-ts/lib/string");
const TE = require("fp-ts/lib/TaskEither");
const t = require("io-ts");
const node_forge_1 = require("node-forge");
const xmlCrypto = require("xml-crypto");
const xml2js_1 = require("xml2js");
const xmldom_1 = require("@xmldom/xmldom");
const config_1 = require("../config");
const lollipop_1 = require("../types/lollipop");
const logger_1 = require("./logger");
const middleware_1 = require("./middleware");
exports.SAML_NAMESPACE = {
ASSERTION: "urn:oasis:names:tc:SAML:2.0:assertion",
PROTOCOL: "urn:oasis:names:tc:SAML:2.0:protocol",
SPID: "https://spid.gov.it/saml-extensions",
XMLDSIG: "http://www.w3.org/2000/09/xmldsig#",
};
exports.XML_TAGS = {
LANG: "xml:lang",
};
exports.SPID_TAGS = {
ENTITY_TYPE: "spid:EntityType",
FISCAL_CODE: "spid:FiscalCode",
IPA_CODE: "spid:IPACode",
VAT_NUMBER: "spid:VATNumber",
};
exports.ISSUER_FORMAT = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity";
const decodeBase64 = (s) => Buffer.from(s, "base64").toString("utf8");
/**
* Remove prefix and suffix from x509 certificate.
*/
const cleanCert = (cert) => cert
.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, "")
.replace(/-+END CERTIFICATE-+\r?\n?/, "")
.replace(/\r\n/g, "\n");
exports.ERROR_SAML_RESPONSE_MISSING = "Missing SAMLResponse in ACS";
const SAMLResponse = t.type({
SAMLResponse: t.string,
});
exports.InfoNotAvailable = "NOT AVAILABLE";
/**
* If an eventHandler and a feature flag are provided this function logs the timing deltas.
* This is useful to monitor the timings and to adjust the clockSkewMs variable
*/
const extractAndLogTimings = (startTime, idpIssuer, requestId, clockSkewMs = 0, eventHandler, hasClockSkewLoggingEvent
// eslint-disable-next-line max-params
) => (info) => {
// when clockSkewMs is set to -1 the validations are always true, so we skip the logs in that case
if (eventHandler && hasClockSkewLoggingEvent && clockSkewMs !== -1) {
const extractNotOnOrAfterDelta = (element) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(element.getAttribute("NotOnOrAfter")), E.chain(dates_1.UTCISODateFromString.decode), E.mapLeft(() => new Error("Could not find/convert NotOnOrAfter")), E.map((NotOnOrAfter) => String(NotOnOrAfter.getTime() - (startTime - clockSkewMs))));
const ResponseIssueInstantClockSkew = String(startTime + clockSkewMs - info.IssueInstant.getTime());
const AssertionIssueInstantClockSkew = String(startTime + clockSkewMs - info.AssertionIssueInstant.getTime());
const AssertionNotBeforeClockSkew = (0, function_1.pipe)(info.Assertion.getAttribute("NotBefore"), strings_1.NonEmptyString.decode, E.chain(dates_1.UTCISODateFromString.decode), E.map((NotBefore) => String(startTime + clockSkewMs - NotBefore.getTime())), E.getOrElseW(() => exports.InfoNotAvailable));
const AssertionSubjectNotOnOrAfterClockSkew = (0, function_1.pipe)(info.Assertion.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "Subject").item(0), O.fromNullable, O.chainNullableK((Subject) => Subject.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "SubjectConfirmation").item(0)), O.chainNullableK((SubjectConfirmation) => SubjectConfirmation.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "SubjectConfirmationData").item(0)), E.fromOption(() => new Error("Could not find elements")), E.chainW(extractNotOnOrAfterDelta), E.getOrElseW(() => exports.InfoNotAvailable));
const AssertionConditionsNotOnOrAfterClockSkew = (0, function_1.pipe)(info.Assertion.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "Conditions").item(0), O.fromNullable, E.fromOption(() => new Error("Could not find elements")), E.chainW(extractNotOnOrAfterDelta), E.getOrElseW(() => exports.InfoNotAvailable));
const timings = {
AssertionConditionsNotOnOrAfterClockSkew,
AssertionIssueInstantClockSkew,
AssertionNotBeforeClockSkew,
AssertionSubjectNotOnOrAfterClockSkew,
ResponseIssueInstantClockSkew,
};
eventHandler({
data: Object.assign({ idpIssuer, message: "Clockskew validations logging", requestId }, timings),
name: "spid.info.clockskew",
type: "INFO",
});
}
return TE.right(void 0);
};
exports.extractAndLogTimings = extractAndLogTimings;
/**
* True if the element contains at least one element signed using hamc
*
* @param e
*/
const isSignedWithHmac = (e) => {
const signatures = e.getElementsByTagNameNS(exports.SAML_NAMESPACE.XMLDSIG, "SignatureMethod");
return Array.from({ length: signatures.length })
.map((_, i) => signatures.item(i))
.some((item) => {
var _a;
return ((_a = item === null || item === void 0 ? void 0 : item.getAttribute("Algorithm")) === null || _a === void 0 ? void 0 : _a.valueOf()) ===
"http://www.w3.org/2000/09/xmldsig#hmac-sha1";
});
};
exports.notSignedWithHmacPredicate = E.fromPredicate(fp_ts_1.predicate.not(isSignedWithHmac), (_) => new Error("HMAC Signature is forbidden"));
const safeXMLParseFromString = (doc) => (0, function_1.pipe)(O.tryCatch(() => new xmldom_1.DOMParser().parseFromString(doc, "text/xml")), O.chain(O.fromNullable));
exports.safeXMLParseFromString = safeXMLParseFromString;
const getXmlFromSamlResponse = (body) => (0, function_1.pipe)(O.fromEither(SAMLResponse.decode(body)), O.map((_) => decodeBase64(_.SAMLResponse)), O.chain(exports.safeXMLParseFromString));
exports.getXmlFromSamlResponse = getXmlFromSamlResponse;
/**
* Extract StatusMessage from SAML response
*
* ie. for <StatusMessage>ErrorCode nr22</StatusMessage>
* returns "22"
*/
const getErrorCodeFromResponse = (doc) => (0, function_1.pipe)(O.fromNullable(doc.getElementsByTagNameNS(exports.SAML_NAMESPACE.PROTOCOL, "StatusMessage")), O.chain((responseStatusMessageEl) => {
var _a;
return ((_a = responseStatusMessageEl === null || responseStatusMessageEl === void 0 ? void 0 : responseStatusMessageEl[0]) === null || _a === void 0 ? void 0 : _a.textContent)
? O.some(responseStatusMessageEl[0].textContent.trim())
: O.none;
}), O.chain((errorString) => {
const indexString = "ErrorCode nr";
const errorCode = errorString.slice(errorString.indexOf(indexString) + indexString.length);
return errorCode !== "" ? O.some(errorCode) : O.none;
}));
exports.getErrorCodeFromResponse = getErrorCodeFromResponse;
/**
* Extracts the issuer field from the response body.
*/
const getSamlIssuer = (doc) => (0, function_1.pipe)(O.fromNullable(doc.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "Issuer").item(0)), O.chainNullableK((_) => { var _a; return (_a = _.textContent) === null || _a === void 0 ? void 0 : _a.trim(); }));
exports.getSamlIssuer = getSamlIssuer;
/**
* Extracts IDP entityID from query parameter (if any).
*
* @returns
* - the certificates (and entrypoint) for the IDP that matches the provided entityID
* - all IDP certificates if no entityID is provided (and no entrypoint)
* - none if no IDP matches the provided entityID
*/
const getEntrypointCerts = (req, idps) => (0, function_1.pipe)(O.fromNullable(req), O.chainNullableK((r) => r.query), O.chainNullableK((q) => q.entityID), O.chain((entityID) =>
// As only strings can be key of an object (other than number and Symbol),
// we have to narrow type to have the compiler accept it
// In the unlikely case entityID is not a string, an empty value is returned
typeof entityID === "string"
? (0, function_1.pipe)(O.fromNullable(idps[entityID]), O.map((idp) => ({
cert: idp.cert,
entryPoint: idp.entryPoint,
idpIssuer: idp.entityID,
})))
: O.none), O.alt(() =>
// collect all IDP certificates in case no entityID is provided
O.some({
cert: (0, function_1.pipe)(idps, (0, Record_1.collect)(string_1.Ord)((_, idp) => (idp === null || idp === void 0 ? void 0 : idp.cert) || []), Array_1.flatten),
// TODO: leave entryPoint undefined when this gets fixed
// @see https://github.com/bergie/passport-saml/issues/415
entryPoint: "",
})));
const getIDFromRequest = (requestXML) => (0, function_1.pipe)((0, exports.safeXMLParseFromString)(requestXML), O.chain((xmlRequest) => O.fromNullable(xmlRequest
.getElementsByTagNameNS(exports.SAML_NAMESPACE.PROTOCOL, "AuthnRequest")
.item(0))), O.chain((AuthnRequest) => O.fromEither(strings_1.NonEmptyString.decode(AuthnRequest.getAttribute("ID")))));
exports.getIDFromRequest = getIDFromRequest;
const getAuthnContextValueFromResponse = (response) => (0, function_1.pipe)(response, exports.safeXMLParseFromString,
// ie. <saml2:AuthnContextClassRef>https://www.spid.gov.it/SpidL2</saml2:AuthnContextClassRef>
O.map((XmlResponse) => XmlResponse.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "AuthnContextClassRef")), O.chain((0, function_1.flow)((responseAuthLevelEl) => { var _a; return (_a = responseAuthLevelEl[0]) === null || _a === void 0 ? void 0 : _a.textContent; }, O.fromNullable, O.map((textContent) => textContent.trim()))));
/**
* Extracts the correct SPID level from response.
*/
const getAuthSalmOptions = (req, decodedResponse) => (0, function_1.pipe)(O.fromNullable(req), O.chainNullableK((r) => r.query), O.chainNullableK((q) => q.authLevel),
// As only strings can be key of SPID_LEVELS record,
// we have to narrow type to have the compiler accept it
// In the unlikely case authLevel is not a string, an empty value is returned
O.filter((e) => typeof e === "string"), O.chain((authLevel) => (0, function_1.pipe)((0, Record_1.lookup)(authLevel, config_1.SPID_LEVELS), O.map((authnContext) => ({
authnContext,
forceAuthn: authLevel !== "SpidL1",
})), O.altW(() => {
logger_1.logger.error("SPID cannot find a valid authnContext for given authLevel: %s", authLevel);
return O.none;
}))), O.alt(() => (0, function_1.pipe)(O.fromNullable(decodedResponse), O.chain((response) => getAuthnContextValueFromResponse(response)), O.chain((authnContext) => (0, function_1.pipe)((0, Record_1.lookup)(authnContext, config_1.SPID_URLS),
// check if the parsed value is a valid SPID AuthLevel
O.map((authLevel) => ({
authnContext,
forceAuthn: authLevel !== "SpidL1",
})), O.altW(() => {
logger_1.logger.error("SPID cannot find a valid authLevel for given authnContext: %s", authnContext);
return O.none;
}))))));
/**
* Reads dates information in x509 certificate
* and logs remaining time to its expiration date.
*
* @param samlCert x509 certificate as string
*/
const logSamlCertExpiration = (samlCert) => {
try {
const out = node_forge_1.pki.certificateFromPem(samlCert);
if (out.validity.notAfter) {
const timeDiff = (0, date_fns_1.distanceInWordsToNow)(out.validity.notAfter);
const warningDate = (0, date_fns_1.subDays)(new Date(), 60);
if ((0, date_fns_1.isAfter)(out.validity.notAfter, warningDate)) {
logger_1.logger.info("samlCert expire in %s", timeDiff);
}
else if ((0, date_fns_1.isAfter)(out.validity.notAfter, new Date())) {
logger_1.logger.warn("samlCert expire in %s", timeDiff);
}
else {
logger_1.logger.error("samlCert expired from %s", timeDiff);
}
}
else {
logger_1.logger.error("Missing expiration date on saml certificate.");
}
}
catch (e) {
logger_1.logger.error("Error calculating saml cert expiration: %s", e);
}
};
exports.logSamlCertExpiration = logSamlCertExpiration;
/**
* This method extracts the correct IDP metadata
* from the passport strategy options.
*
* It's executed for every SPID login (when passport
* middleware is configured) and when generating
* the Service Provider metadata.
*/
const getSamlOptions = (req, done) => {
var _a;
try {
// Get decoded response
const decodedResponse = ((_a = req.body) === null || _a === void 0 ? void 0 : _a.SAMLResponse)
? decodeBase64(req.body.SAMLResponse)
: undefined;
// Get SPID strategy options with IDPs metadata
const maybeSpidStrategyOptions = O.fromNullable((0, middleware_1.getSpidStrategyOption)(req.app));
if (O.isNone(maybeSpidStrategyOptions)) {
throw new Error("Missing Spid Strategy Option configuration inside express App");
}
// Get the correct entry within the IDP metadata object
const maybeEntrypointCerts = (0, function_1.pipe)(maybeSpidStrategyOptions, O.chain((spidStrategyOptions) => getEntrypointCerts(req, spidStrategyOptions.idp)));
if (O.isNone(maybeEntrypointCerts)) {
logger_1.logger.debug(`SPID cannot find a valid idp in spidOptions for given entityID: ${req.query.entityID}`);
}
const entrypointCerts = (0, function_1.pipe)(maybeEntrypointCerts, O.getOrElse(() => ({ cert: [] })));
// Get authnContext (SPID level) and forceAuthn from request payload
const maybeAuthOptions = getAuthSalmOptions(req, decodedResponse);
if (O.isNone(maybeAuthOptions)) {
logger_1.logger.debug("SPID cannot find authnContext in response %s", decodedResponse);
}
const authOptions = (0, function_1.pipe)(maybeAuthOptions, O.getOrElseW(() => ({})));
const options = Object.assign(Object.assign(Object.assign(Object.assign({}, maybeSpidStrategyOptions.value.sp), authOptions), entrypointCerts), { cert: Array.from(entrypointCerts.cert) });
return done(null, options);
}
catch (e) {
return done(e);
}
};
exports.getSamlOptions = getSamlOptions;
//
// Service Provider Metadata
//
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const getSpidAttributesMetadata = (serviceProviderConfig) => serviceProviderConfig.requiredAttributes
? serviceProviderConfig.requiredAttributes.attributes.map((item) => ({
$: {
FriendlyName: config_1.SPID_USER_ATTRIBUTES[item] || "",
Name: item,
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
},
}))
: [];
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const getSpidOrganizationMetadata = (serviceProviderConfig) => serviceProviderConfig.organization
? {
Organization: {
OrganizationName: {
$: { [exports.XML_TAGS.LANG]: "it" },
_: serviceProviderConfig.organization.name,
},
// must appear after organization name
// eslint-disable-next-line sort-keys
OrganizationDisplayName: {
$: { [exports.XML_TAGS.LANG]: "it" },
_: serviceProviderConfig.organization.displayName,
},
OrganizationURL: {
$: { [exports.XML_TAGS.LANG]: "it" },
_: serviceProviderConfig.organization.URL,
},
},
}
: {};
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const getSpidContactPersonMetadata = (serviceProviderConfig
// eslint-disable-next-line sonarjs/cognitive-complexity
) => serviceProviderConfig.contacts
? serviceProviderConfig.contacts
.map((item) => {
var _a;
const contact = Object.assign({ $: {
contactType: item.contactType,
}, Company: item.company, EmailAddress: item.email }, (item.phone ? { TelephoneNumber: item.phone } : {}));
if (item.contactType === middleware_1.ContactType.OTHER) {
return Object.assign(Object.assign({ Extensions: Object.assign(Object.assign(Object.assign(Object.assign({}, (item.extensions.IPACode
? { [exports.SPID_TAGS.IPA_CODE]: item.extensions.IPACode }
: {})), (item.extensions.VATNumber
? { [exports.SPID_TAGS.VAT_NUMBER]: item.extensions.VATNumber }
: {})), (((_a = item.extensions) === null || _a === void 0 ? void 0 : _a.FiscalCode)
? { [exports.SPID_TAGS.FISCAL_CODE]: item.extensions.FiscalCode }
: {})), (item.entityType === middleware_1.EntityType.AGGREGATOR
? { [`spid:${item.extensions.aggregatorType}`]: {} }
: {})) }, contact), { $: Object.assign(Object.assign({}, contact.$), { [exports.SPID_TAGS.ENTITY_TYPE]: item.entityType }) });
}
return contact;
})
// Contacts array is limited to 3 elements
.slice(0, 3)
: {};
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const getKeyInfoForMetadata = (publicCert, privateKey) => ({
file: privateKey,
getKey: () => Buffer.from(privateKey),
getKeyInfo: () => `<X509Data><X509Certificate>${publicCert}</X509Certificate></X509Data>`,
});
const getMetadataTamperer = (xmlBuilder, serviceProviderConfig, samlConfig) => (generateXml) => (0, function_1.pipe)(TE.tryCatch(() => (0, xml2js_1.parseStringPromise)(generateXml), E.toError), TE.chain((o) => TE.tryCatch(() => __awaiter(void 0, void 0, void 0, function* () {
// it is safe to mutate object here since it is
// deserialized and serialized locally in this method
const sso = o.EntityDescriptor.SPSSODescriptor[0];
// eslint-disable-next-line functional/immutable-data
sso.$ = Object.assign(Object.assign({}, sso.$), { AuthnRequestsSigned: true, WantAssertionsSigned: true });
// eslint-disable-next-line functional/immutable-data
sso.AssertionConsumerService[0].$.index = 0;
// eslint-disable-next-line functional/immutable-data
sso.AttributeConsumingService = {
$: {
index: samlConfig.attributeConsumingServiceIndex,
},
ServiceName: {
$: {
[exports.XML_TAGS.LANG]: "it",
},
_: serviceProviderConfig.requiredAttributes.name,
},
// must appear after attributes
// eslint-disable-next-line sort-keys
RequestedAttribute: getSpidAttributesMetadata(serviceProviderConfig),
};
// eslint-disable-next-line functional/immutable-data
o.EntityDescriptor = Object.assign(Object.assign({}, o.EntityDescriptor), getSpidOrganizationMetadata(serviceProviderConfig));
if (serviceProviderConfig.contacts) {
// eslint-disable-next-line functional/immutable-data
o.EntityDescriptor = Object.assign(Object.assign({}, o.EntityDescriptor), { $: Object.assign(Object.assign({}, o.EntityDescriptor.$), {
// eslint-disable-next-line @typescript-eslint/naming-convention
"xmlns:spid": exports.SAML_NAMESPACE.SPID }), ContactPerson: getSpidContactPersonMetadata(serviceProviderConfig) });
}
return o;
}), E.toError)), TE.chain((_) => TE.tryCatch(() => __awaiter(void 0, void 0, void 0, function* () { return xmlBuilder.buildObject(_); }), E.toError)), TE.chain((xml) => TE.tryCatch(() => __awaiter(void 0, void 0, void 0, function* () {
// sign xml metadata
if (!samlConfig.privateCert) {
throw new Error("You must provide a private key to sign SPID service provider metadata.");
}
const sig = new xmlCrypto.SignedXml();
const publicCert = cleanCert(serviceProviderConfig.publicCert);
// eslint-disable-next-line functional/immutable-data
sig.keyInfoProvider = getKeyInfoForMetadata(publicCert, samlConfig.privateCert);
// eslint-disable-next-line functional/immutable-data
sig.signatureAlgorithm =
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
// eslint-disable-next-line functional/immutable-data
sig.signingKey = samlConfig.privateCert;
sig.addReference("//*[local-name(.)='EntityDescriptor']", [
"http://www.w3.org/2000/09/xmldsig#enveloped-signature",
"http://www.w3.org/2001/10/xml-exc-c14n#",
], "http://www.w3.org/2001/04/xmlenc#sha256");
sig.computeSignature(xml, {
// Place the signature tag before all other tags
// eslint-disable-next-line sort-keys
location: { reference: "", action: "prepend" },
});
return sig.getSignedXml();
}), E.toError)));
exports.getMetadataTamperer = getMetadataTamperer;
//
// Authorize request
//
const getAuthorizeRequestTamperer = (xmlBuilder, samlConfig) => (generateXml, lollipopParams) => (0, function_1.pipe)(TE.tryCatch(() => (0, xml2js_1.parseStringPromise)(generateXml), E.toError), TE.chain((o) => (0, function_1.pipe)(lollipopParams, O.fromNullable, E.fromOption(() => o), E.fold(TE.right, (lParams) => (0, function_1.pipe)(lParams.hashAlgorithm, O.fromNullable, O.getOrElse(() => lollipop_1.DEFAULT_LOLLIPOP_HASH_ALGORITHM), TE.of, TE.bindTo("hashingAlgo"), TE.bind("jwkThumbprint", ({ hashingAlgo }) => TE.tryCatch(() => jose.calculateJwkThumbprint(lParams.pubKey, hashingAlgo), E.toError)), TE.map(({ hashingAlgo, jwkThumbprint }) => `${hashingAlgo}-${jwkThumbprint}`), TE.chain((requestId) => TE.tryCatch(() => __awaiter(void 0, void 0, void 0, function* () {
// eslint-disable-next-line functional/immutable-data
o["samlp:AuthnRequest"].$.ID = requestId;
return o;
}), E.toError)))))), TE.chain((o) => TE.tryCatch(() => __awaiter(void 0, void 0, void 0, function* () {
// it is safe to mutate object here since it is
// deserialized and serialized locally in this method
const authnRequest = o["samlp:AuthnRequest"];
// eslint-disable-next-line fp/no-delete, functional/immutable-data
delete authnRequest["samlp:NameIDPolicy"][0].$.AllowCreate;
// eslint-disable-next-line functional/immutable-data
authnRequest["saml:Issuer"][0].$.NameQualifier = samlConfig.issuer;
// eslint-disable-next-line functional/immutable-data
authnRequest["saml:Issuer"][0].$.Format = exports.ISSUER_FORMAT;
return o;
}), E.toError)), TE.chain((obj) => TE.tryCatch(() => __awaiter(void 0, void 0, void 0, function* () { return xmlBuilder.buildObject(obj); }), E.toError)));
exports.getAuthorizeRequestTamperer = getAuthorizeRequestTamperer;
//
// Validate response
//
const utcStringToDate = (value, tag) => (0, function_1.pipe)(dates_1.UTCISODateFromString.decode(value), E.mapLeft(() => new Error(`${tag} must be an UTCISO format date string`)));
const validateIssuer = (fatherElement, idpIssuer) => (0, function_1.pipe)(E.fromOption(() => new Error("Issuer element must be present"))(O.fromNullable(fatherElement
.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "Issuer")
.item(0))), E.chain((Issuer) => {
var _a;
return (0, function_1.pipe)(strings_1.NonEmptyString.decode((_a = Issuer.textContent) === null || _a === void 0 ? void 0 : _a.trim()), E.mapLeft(() => new Error("Issuer element must be not empty")), E.chain(E.fromPredicate((IssuerTextContent) => IssuerTextContent === idpIssuer, () => new Error(`Invalid Issuer. Expected value is ${idpIssuer}`))), E.map(() => Issuer));
}));
exports.validateIssuer = validateIssuer;
const mainAttributeValidation = (validationTimestamp) => (requestOrAssertion, acceptedClockSkewMs = 0) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(requestOrAssertion.getAttribute("ID")), E.mapLeft(() => new Error("Assertion must contain a non empty ID")), E.map(() => requestOrAssertion.getAttribute("Version")), E.chain(E.fromPredicate((Version) => Version === "2.0", () => new Error("Version version must be 2.0"))), E.chain(() => E.fromOption(() => new Error("Assertion must contain a non empty IssueInstant"))(O.fromNullable(requestOrAssertion.getAttribute("IssueInstant")))), E.chain((IssueInstant) => utcStringToDate(IssueInstant, "IssueInstant")), E.chain(E.fromPredicate((IssueInstant) => IssueInstant.getTime() <
(acceptedClockSkewMs === -1
? Infinity
: validationTimestamp + acceptedClockSkewMs), () => new Error("IssueInstant must be in the past"))));
exports.mainAttributeValidation = mainAttributeValidation;
const isEmptyNode = (element) => {
if (element.childNodes.length > 1) {
return false;
}
else if (element.firstChild &&
element.firstChild.nodeType === element.ELEMENT_NODE
// eslint-disable-next-line sonarjs/no-duplicated-branches
) {
return false;
}
else if (element.textContent &&
// eslint-disable-next-line no-useless-escape
element.textContent.replace(/[\r\n\ ]+/g, "") !== ""
// eslint-disable-next-line sonarjs/no-duplicated-branches
) {
return false;
}
return true;
};
exports.isEmptyNode = isEmptyNode;
const isOverflowNumberOf = (elemArray, maxNumberOfChildren) => elemArray.filter((e) => e.nodeType === e.ELEMENT_NODE).length >
maxNumberOfChildren;
exports.TransformError = t.interface({
idpIssuer: t.string,
message: t.string,
numberOfTransforms: t.number,
});
const transformsValidation = (targetElement, idpIssuer) => (0, function_1.pipe)(O.fromPredicate((elements) => elements.length > 0)(Array.from(targetElement.getElementsByTagNameNS(exports.SAML_NAMESPACE.XMLDSIG, "Transform"))), O.fold(() => E.right(targetElement), (transformElements) => (0, function_1.pipe)(E.fromPredicate((_) => !isOverflowNumberOf(_, 4), (_) => exports.TransformError.encode({
idpIssuer,
message: "Transform element cannot occurs more than 4 times",
numberOfTransforms: _.length,
}))(transformElements), E.map(() => targetElement))));
exports.transformsValidation = transformsValidation;
const notOnOrAfterValidation = (validationTimestamp) => (element, acceptedClockSkewMs = 0) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(element.getAttribute("NotOnOrAfter")), E.mapLeft(() => new Error("NotOnOrAfter attribute must be a non empty string")), E.chain((NotOnOrAfter) => utcStringToDate(NotOnOrAfter, "NotOnOrAfter")), E.chain(E.fromPredicate((NotOnOrAfter) => NotOnOrAfter.getTime() >
(acceptedClockSkewMs === -1
? -Infinity
: validationTimestamp - acceptedClockSkewMs), () => new Error("NotOnOrAfter must be in the future"))));
const assertionValidation =
// eslint-disable-next-line max-lines-per-function, prettier/prettier
(validationTimestamp) =>
// eslint-disable-next-line max-lines-per-function
(Assertion, samlConfig, InResponseTo, requestAuthnContextClassRef
// eslint-disable-next-line sonarjs/cognitive-complexity
) => {
const acceptedClockSkewMs = samlConfig.acceptedClockSkewMs || 0;
return (0, function_1.pipe)(E.fromOption(() => new Error("Assertion must be signed"))(O.fromNullable(Assertion.getElementsByTagNameNS(exports.SAML_NAMESPACE.XMLDSIG, "Signature").item(0))), E.chain(exports.notSignedWithHmacPredicate),
// eslint-disable-next-line max-lines-per-function
E.chain(() => (0, function_1.pipe)(E.fromOption(() => new Error("Subject element must be present"))(O.fromNullable(Assertion.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "Subject").item(0))), E.chain(E.fromPredicate(fp_ts_1.predicate.not(exports.isEmptyNode), () => new Error("Subject element must be not empty"))), E.chain((Subject) => (0, function_1.pipe)(E.fromOption(() => new Error("NameID element must be present"))(O.fromNullable(Subject.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "NameID").item(0))), E.chain(E.fromPredicate(fp_ts_1.predicate.not(exports.isEmptyNode), () => new Error("NameID element must be not empty"))), E.chain((NameID) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(NameID.getAttribute("Format")), E.mapLeft(() => new Error("Format attribute of NameID element must be a non empty string")), E.chain(E.fromPredicate((Format) => Format ===
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient", () => new Error("Format attribute of NameID element is invalid"))), E.map(() => NameID))), E.chain((NameID) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(NameID.getAttribute("NameQualifier")), E.mapLeft(() => new Error("NameQualifier attribute of NameID element must be a non empty string")))), E.map(() => Subject))), E.chain((Subject) => (0, function_1.pipe)(E.fromOption(() => new Error("SubjectConfirmation element must be present"))(O.fromNullable(Subject.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "SubjectConfirmation").item(0))), E.chain(E.fromPredicate(fp_ts_1.predicate.not(exports.isEmptyNode), () => new Error("SubjectConfirmation element must be not empty"))), E.chain((SubjectConfirmation) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(SubjectConfirmation.getAttribute("Method")), E.mapLeft(() => new Error("Method attribute of SubjectConfirmation element must be a non empty string")), E.chain(E.fromPredicate((Method) => Method === "urn:oasis:names:tc:SAML:2.0:cm:bearer", () => new Error("Method attribute of SubjectConfirmation element is invalid"))), E.map(() => SubjectConfirmation))), E.chain((SubjectConfirmation) => (0, function_1.pipe)(E.fromOption(() => new Error("SubjectConfirmationData element must be provided"))(O.fromNullable(SubjectConfirmation.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "SubjectConfirmationData").item(0))), E.chain((SubjectConfirmationData) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(SubjectConfirmationData.getAttribute("Recipient")), E.mapLeft(() => new Error("Recipient attribute of SubjectConfirmationData element must be a non empty string")), E.chain(E.fromPredicate((Recipient) => Recipient === samlConfig.callbackUrl, () => new Error("Recipient attribute of SubjectConfirmationData element must be equal to AssertionConsumerServiceURL"))), E.map(() => SubjectConfirmationData))), E.chain((SubjectConfirmationData) => (0, function_1.pipe)(notOnOrAfterValidation(validationTimestamp)(SubjectConfirmationData, acceptedClockSkewMs), E.map(() => SubjectConfirmationData))), E.chain((SubjectConfirmationData) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(SubjectConfirmationData.getAttribute("InResponseTo")), E.mapLeft(() => new Error("InResponseTo attribute of SubjectConfirmationData element must be a non empty string")), E.chain(E.fromPredicate((inResponseTo) => inResponseTo === InResponseTo, () => new Error("InResponseTo attribute of SubjectConfirmationData element must be equal to Response InResponseTo"))))))))),
// eslint-disable-next-line max-lines-per-function
E.chain(() => (0, function_1.pipe)(E.fromOption(() => new Error("Conditions element must be provided"))(O.fromNullable(Assertion.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "Conditions").item(0))), E.chain(E.fromPredicate(fp_ts_1.predicate.not(exports.isEmptyNode), () => new Error("Conditions element must be provided"))), E.chain((Conditions) => (0, function_1.pipe)(notOnOrAfterValidation(validationTimestamp)(Conditions, acceptedClockSkewMs), E.map(() => Conditions))), E.chainFirst((Conditions) => (0, function_1.pipe)(strings_1.NonEmptyString.decode(Conditions.getAttribute("NotBefore")), E.mapLeft(() => new Error("NotBefore must be a non empty string")), E.chain((NotBefore) => utcStringToDate(NotBefore, "NotBefore")), E.chain(E.fromPredicate((NotBefore) => NotBefore.getTime() <=
(acceptedClockSkewMs === -1
? Infinity
: validationTimestamp + acceptedClockSkewMs), () => new Error("NotBefore must be in the past"))))), E.chain((Conditions) => (0, function_1.pipe)(E.fromOption(() => new Error("AudienceRestriction element must be present and not empty"))(O.fromNullable(Conditions.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "AudienceRestriction").item(0))), E.chain(E.fromPredicate(fp_ts_1.predicate.not(exports.isEmptyNode),
// eslint-disable-next-line sonarjs/no-identical-functions
() => new Error("AudienceRestriction element must be present and not empty"))), E.chain((AudienceRestriction) => (0, function_1.pipe)(E.fromOption(() => new Error("Audience missing"))(O.fromNullable(AudienceRestriction.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "Audience").item(0))), E.chain(E.fromPredicate((Audience) => {
var _a;
return ((_a = Audience.textContent) === null || _a === void 0 ? void 0 : _a.trim()) ===
samlConfig.issuer;
}, () => new Error("Audience invalid"))))))), E.chain(() => (0, function_1.pipe)(E.fromOption(() => new Error("Missing AuthnStatement"))(O.fromNullable(Assertion.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "AuthnStatement").item(0))), E.chain(E.fromPredicate(fp_ts_1.predicate.not(exports.isEmptyNode), () => new Error("Empty AuthnStatement"))), E.chain((AuthnStatement) => (0, function_1.pipe)(E.fromOption(() => new Error("Missing AuthnContext"))(O.fromNullable(AuthnStatement.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "AuthnContext").item(0))), E.chain(E.fromPredicate(fp_ts_1.predicate.not(exports.isEmptyNode), () => new Error("Empty AuthnContext"))), E.chain((AuthnContext) => (0, function_1.pipe)(E.fromOption(() => new Error("Missing AuthnContextClassRef"))(O.fromNullable(AuthnContext.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "AuthnContextClassRef").item(0))), E.chain(E.fromPredicate(fp_ts_1.predicate.not(exports.isEmptyNode), () => new Error("Empty AuthnContextClassRef"))), E.map((AuthnContextClassRef) => { var _a; return (_a = AuthnContextClassRef.textContent) === null || _a === void 0 ? void 0 : _a.trim(); }), E.chain(E.fromPredicate((AuthnContextClassRef) => AuthnContextClassRef === config_1.SPID_LEVELS.SpidL1 ||
AuthnContextClassRef === config_1.SPID_LEVELS.SpidL2 ||
AuthnContextClassRef === config_1.SPID_LEVELS.SpidL3, () => new Error("Invalid AuthnContextClassRef value"))), E.chain(E.fromPredicate((AuthnContextClassRef) => requestAuthnContextClassRef ===
config_1.SPID_LEVELS.SpidL2
? AuthnContextClassRef ===
config_1.SPID_LEVELS.SpidL2 ||
AuthnContextClassRef ===
config_1.SPID_LEVELS.SpidL3
: requestAuthnContextClassRef ===
config_1.SPID_LEVELS.SpidL1
? AuthnContextClassRef ===
config_1.SPID_LEVELS.SpidL1 ||
AuthnContextClassRef ===
config_1.SPID_LEVELS.SpidL2 ||
AuthnContextClassRef ===
config_1.SPID_LEVELS.SpidL3
: requestAuthnContextClassRef ===
AuthnContextClassRef, () => new Error("AuthnContextClassRef value not expected"))))))))), E.chain(() => (0, function_1.pipe)(E.fromOption(() => new Error("AttributeStatement must contains Attributes"))((0, function_1.pipe)(O.fromNullable(Assertion.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "AttributeStatement").item(0)), O.map((AttributeStatement) => AttributeStatement.getElementsByTagNameNS(exports.SAML_NAMESPACE.ASSERTION, "Attribute")))), E.chain(E.fromPredicate((Attributes) => Attributes.length > 0 &&
!Array.from(Attributes).some(exports.isEmptyNode), () => new Error("Attribute element must be present and not empty"))))))))));
};
exports.assertionValidation = assertionValidation;
//# sourceMappingURL=samlUtils.js.map