saml-login
Version:
SAML 2.0 implementation for Node.js
561 lines • 30.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const zlib = require("zlib");
const url_1 = require("url");
const querystring = require("querystring");
const util = require("util");
const types_1 = require("./types");
const utility_1 = require("./utility");
const xml_1 = require("./xml");
const crypto_1 = require("./crypto");
const datetime_1 = require("./datetime");
const deflateRaw = util.promisify(zlib.deflateRaw);
const inflateRaw = util.promisify(zlib.inflateRaw);
// async function processValidlySignedSamlLogout(
// doc: XMLOutput,
// dom: Document
// ): Promise<{ profile?: Profile | null; loggedOut?: boolean }> {
// const response = doc.LogoutResponse;
// const request = doc.LogoutRequest;
// if (response) {
// return { profile: null, loggedOut: true };
// } else if (request) {
// return await processValidlySignedPostRequest( doc, dom);
// } else {
// throw new Error("Unknown SAML response message");
// }
// }
class SamlLogin {
constructor() {
this.requestIdExpirationPeriodMs = 28800000;
}
async generateDelegationUrl(options) {
const id = (0, crypto_1.generateUniqueId)();
const assertionId = (0, crypto_1.generateUniqueId)();
const instantDateTime = options.requestTimestamp || new Date();
const clockSkewDateTime = new Date(instantDateTime);
clockSkewDateTime.setTime(clockSkewDateTime.getTime() - 5 * 60 * 1000);
const expiryDateTime = new Date(instantDateTime);
expiryDateTime.setTime(expiryDateTime.getTime() + 30 * 60 * 1000);
const xmlResponse = {
"samlp:Response": {
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
"@ID": id,
"@Version": "2.0",
"@IssueInstant": instantDateTime.toISOString(),
// "@ProtocolBinding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
"@Destination": options.applicationAssertionConsumerServiceUrl,
"saml:Issuer": {
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
"#text": options.issuerEntityId,
},
"samlp:Status": {
"samlp:StatusCode": {
"@Value": "urn:oasis:names:tc:SAML:2.0:status:Success",
}
},
"saml:Assertion": {
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
"@ID": assertionId,
"@Version": "2.0",
"@IssueInstant": instantDateTime.toISOString(),
"saml:Issuer": {
"#text": options.issuerEntityId,
},
"saml:Subject": {
"saml:NameID": {
"@Format": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"#text": options.userId
},
"saml:SubjectConfirmation": {
"@Method": "urn:oasis:names:tc:SAML:2.0:cm:bearer",
"saml:SubjectConfirmationData": {
"@NotOnOrAfter": expiryDateTime.toISOString(),
"@Recipient": options.applicationAssertionConsumerServiceUrl
}
}
},
"saml:Conditions": {
"@NotBefore": clockSkewDateTime.toISOString(),
"@NotOnOrAfter": expiryDateTime.toISOString(),
"saml:AudienceRestriction": {
"saml:Audience": options.applicationEntityId
}
},
"saml:AttributeStatement": {
"saml:Attribute": {
"@Name": "userId"
}
},
"saml:AuthnStatement": {
"@AuthnInstant": instantDateTime.toISOString(),
"@SessionIndex": assertionId,
"saml:AuthnContext": {
"saml:AuthnContextClassRef": "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified"
}
}
}
},
};
if (options.state) {
xmlResponse['samlp:Response']['@InResponseTo'] = options.state;
}
const unsignedResponse = (0, xml_1.buildXmlBuilderObject)(xmlResponse, false);
const signingOptions = {
privateKey: (0, crypto_1.keyToPEM)(options.privateKey)
};
const signedResponse = (0, utility_1.signXmlResponse)(unsignedResponse, signingOptions);
const target = new url_1.URL(options.applicationAssertionConsumerServiceUrl);
target.searchParams.set('SAMLResponse', Buffer.from(signedResponse).toString('base64'));
target.searchParams.set('RelayState', options.state || '');
// To test verify signature we just created:
// const validationOptions = {
// providerCertificate: options.publicKey,
// expectedProviderIssuer: options.issuerEntityId,
// applicationEntityId: options.applicationEntityId
// };
// const { profile } = await this.validatePostResponse(validationOptions, target.searchParams.toString());
return target.toString();
}
async generateAuthenticationUrl(options) {
const providerSingleSignOnUrl = (0, utility_1.assertRequired)(options.providerSingleSignOnUrl, "The provider's ACS URL `providerSingleSignOnUrl` is required");
const id = options.authenticationRequestId || (0, crypto_1.generateUniqueId)();
const instant = (options.requestTimestamp || new Date()).toISOString();
const xmlRequest = {
"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": providerSingleSignOnUrl,
"saml:Issuer": {
"@xmlns:saml": "urn:oasis:names:tc:SAML:2.0:assertion",
"#text": options.applicationEntityId,
},
},
};
xmlRequest["samlp:AuthnRequest"]["@AssertionConsumerServiceURL"] = options.applicationCallbackAssertionConsumerServiceUrl;
xmlRequest["samlp:AuthnRequest"]["samlp:NameIDPolicy"] = {
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
"@Format": 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
"@AllowCreate": options.allowCreate ? "true" : "false",
};
xmlRequest["samlp:AuthnRequest"]["samlp:RequestedAuthnContext"] = {
"@xmlns:samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
"@Comparison": 'exact',
"saml:AuthnContextClassRef": [],
};
const request = (0, xml_1.buildXmlBuilderObject)(xmlRequest, false);
const buffer = await deflateRaw(request);
const target = new url_1.URL(options.providerSingleSignOnUrl);
target.searchParams.set('SAMLRequest', buffer.toString("base64"));
return target.toString();
}
// This function checks that the |currentNode| in the |fullXml| document contains exactly 1 valid
// signature of the |currentNode|.
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 xpathTransformQuery = ".//*[" +
"local-name(.)='Transform' and " +
"namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#' and " +
"ancestor::*[local-name(.)='Reference' and @URI='#" +
currentNode.getAttribute("ID") +
"']" +
"]";
const transforms = xml_1.xpath.selectElements(currentNode, xpathTransformQuery);
// Reject also XMLDSIG with more than 2 Transform
if (transforms.length > 2) {
// do not return false, throw an error so that it can be caught by tests differently
throw new Error("Invalid signature, too many transforms");
}
const signature = signatures[0];
return certs && certs.filter(c => c).some((certToCheck) => {
return (0, xml_1.validateXmlSignatureForCert)(signature, (0, crypto_1.certToPEM)(certToCheck), fullXml, currentNode);
});
}
async parseSamlRequestMetadata(samlEncodedBody) {
const container = new URLSearchParams(samlEncodedBody);
const samlRequest = container.get('SAMLRequest');
if (!samlRequest) {
const error = Error('SAMLRequest not specified');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidSamlRequest';
throw error;
}
const buffer = Buffer.from(samlRequest, "base64");
const xmlBuffer = await inflateRaw(buffer);
const parsedResult = await (0, xml_1.parseXml2JsFromString)(xmlBuffer.toString());
return {
requestedIssuerEntityId: parsedResult.AuthnRequest.$.Destination,
applicationAssertionConsumerServiceUrl: parsedResult.AuthnRequest.$.AssertionConsumerServiceURL,
requestTimestap: new Date(parsedResult.AuthnRequest.$.IssueInstant),
applicationEntityId: parsedResult.AuthnRequest.Issuer[0]._
};
}
async getSamlAssertionMetadata(samlEncodedBody) {
const container = new URLSearchParams(samlEncodedBody);
const samlResponse = container.get('SAMLResponse');
if (!samlResponse) {
const error = Error('SAMLResponse not specified');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidSamlResponse';
throw error;
}
const xml = Buffer.from(samlResponse, "base64").toString('utf8');
const parsedResult = await (0, xml_1.parseXml2JsFromString)(xml);
const response = parsedResult.Response;
const status = response.Status;
const statusCode = status?.[0].StatusCode;
if (statusCode && statusCode[0].$.Value) {
const msgType = statusCode[0].$.Value.match(/[^:]*$/)[0];
if (msgType != "Success") {
const statusCodeResolvedErrorCode = statusCode[0].StatusCode?.[0].$.Value.match(/[^:]*$/)[0];
const statusMessage = status[0].StatusMessage?.[0]._;
const errorMessage = statusMessage ?? statusCodeResolvedErrorCode ?? 'unspecified';
throw new types_1.ErrorWithXmlStatus("SAML provider returned error: " + errorMessage, statusCodeResolvedErrorCode);
}
}
const inResponseToRaw = parsedResult?.Response?.$?.InResponseTo;
// * If the source idp encoded ~ as _x007E_ then swap it back, they could be also incorrectly encoding other values, but it doesn't seem like there is a standard on this.
const inResponseTo = inResponseToRaw?.includes('~') ? inResponseToRaw : inResponseToRaw?.replace(/_x007E_/gi, '~');
return {
issuerEntityId: parsedResult.Response.Issuer[0]._,
applicationEntityId: parsedResult.Response.$.Destination,
authenticationRequestId: inResponseTo
};
}
async validatePostResponse(options, samlEncodedBody) {
const container = querystring.decode(samlEncodedBody);
const xml = Buffer.from(container.SAMLResponse, "base64").toString("utf8");
const doc = (0, xml_1.parseDomFromString)(xml);
if (!Object.prototype.hasOwnProperty.call(doc, "documentElement")) {
throw new Error("SAMLResponse is not valid base64-encoded XML");
}
const issuersXml = xml_1.xpath.selectElements(doc, "/*[local-name()='Response']/*[local-name()='Issuer']");
const issuerResult = await (0, xml_1.parseXml2JsFromString)(issuersXml.toString());
const issuer = issuerResult?.Issuer?._ || issuerResult?.Issuer?.[0]?._;
if (options.expectedProviderIssuer && issuer && issuer !== options.expectedProviderIssuer) {
const error = new Error("Unknown SAML issuer. Expected: " + options.expectedProviderIssuer + " Received: " + issuer);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidIssuer';
throw error;
}
if (options.requestTimestamp && new Date().getTime() > options.requestTimestamp.getTime() + this.requestIdExpirationPeriodMs) {
const error = new Error("ExpiredRequest");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'ExpiredRequest';
throw error;
}
const certs = !Array.isArray(options.providerCertificate) ? [options.providerCertificate] : options.providerCertificate;
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
const error = new Error("Invalid signature: multiple assertions");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidSignature';
throw error;
}
if (assertions.length) {
try {
if (!this.validateSignature(xml, doc.documentElement, certs) && !this.validateSignature(xml, assertions[0], certs)) {
const error = new Error("Invalid signature");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidSignature';
throw error;
}
}
catch (error) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2571
if (error.code === 'ERR_OSSL_PEM_BAD_BASE64_DECODE') {
const e = new Error("Invalid certificate");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
e.code = 'InvalidCertificate';
throw e;
}
throw error;
}
const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
const inResponseTo = inResponseToNodes && inResponseToNodes[0] && inResponseToNodes[0].nodeValue;
if (!inResponseTo) {
const error = new Error("InResponseTo is not valid");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidResponse';
throw error;
}
return await this.processValidlySignedAssertion(assertions[0].toString(), xml, inResponseTo, options.applicationEntityId);
}
if (encryptedAssertions.length) {
const applicationPrivateKey = (0, utility_1.assertRequired)(options.applicationPrivateKey, "No decryption key for encrypted SAML response");
const encryptedAssertionXml = encryptedAssertions[0].toString();
const decryptedXml = await (0, xml_1.decryptXml)(encryptedAssertionXml, applicationPrivateKey);
const decryptedDoc = (0, 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.validateSignature(xml, doc.documentElement, certs) && !this.validateSignature(decryptedXml, decryptedAssertions[0], certs)) {
throw new Error("Invalid signature from encrypted assertion");
}
const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
const inResponseTo = inResponseToNodes && inResponseToNodes[0] && inResponseToNodes[0].nodeValue;
if (!inResponseTo) {
const error = new Error("InResponseTo is not valid");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidResponse';
throw error;
}
return await this.processValidlySignedAssertion(decryptedAssertions[0].toString(), xml, inResponseTo, options.applicationEntityId);
}
// If there's no assertion, fall back on xml2js response parsing for the status & LogoutResponse code.
if (!this.validateSignature(xml, doc.documentElement, certs)) {
throw new Error("Invalid signature: No response found");
}
const xmlJsDoc = await (0, xml_1.parseXml2JsFromString)(xml);
if (xmlJsDoc.LogoutResponse) {
const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
const inResponseTo = inResponseToNodes && inResponseToNodes[0] && inResponseToNodes[0].nodeValue;
if (!inResponseTo) {
const error = new Error("InResponseTo is not valid");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidResponse';
throw error;
}
return { profile: null, loggedOut: true };
}
const response = xmlJsDoc.Response;
const assertion = response.Assertion;
const status = response.Status;
if (assertion || !status) {
throw new Error("Missing valid SAML assertion");
}
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") {
const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
const inResponseTo = inResponseToNodes && inResponseToNodes[0] && inResponseToNodes[0].nodeValue;
if (!inResponseTo) {
const error = new Error("InResponseTo is not valid");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidResponse';
throw error;
}
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") {
const statusCodeResolvedErrorCode = statusCode[0].StatusCode?.[0].$.Value.match(/[^:]*$/)[0];
const statusMessage = status[0].StatusMessage?.[0]._;
const errorMessage = statusMessage ?? statusCodeResolvedErrorCode ?? 'unspecified';
throw new types_1.ErrorWithXmlStatus("SAML provider returned error: " + errorMessage, statusCodeResolvedErrorCode);
}
}
const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
const inResponseTo = inResponseToNodes && inResponseToNodes[0] && inResponseToNodes[0].nodeValue;
if (!inResponseTo) {
const error = new Error("InResponseTo is not valid");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidResponse';
throw error;
}
throw new Error("Missing valid SAML assertion");
}
async processValidlySignedAssertion(xml, samlResponseXml, inResponseTo, applicationEntityId) {
const profile = {};
const doc = await (0, xml_1.parseXml2JsFromString)(xml);
const assertion = doc.Assertion;
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];
confirmData = subjectConfirmation && subjectConfirmation.SubjectConfirmationData && subjectConfirmation.SubjectConfirmationData[0];
if (subjectConfirmation && subject[0].SubjectConfirmation.length > 1) {
throw new Error("Unable to process multiple SubjectConfirmations in SAML assertion");
}
if (subjectConfirmation) {
if (confirmData && confirmData.$) {
const subjectNotBefore = confirmData.$.NotBefore;
const subjectNotOnOrAfter = confirmData.$.NotOnOrAfter;
const maxTimeLimitMs = this.processMaxAgeAssertionTime(this.requestIdExpirationPeriodMs, subjectNotOnOrAfter, assertion.$.IssueInstant);
const subjErr = this.checkTimestampsValidityError(subjectNotBefore, subjectNotOnOrAfter, maxTimeLimitMs);
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 (subjectConfirmation && confirmData && confirmData.$) {
const subjectInResponseTo = confirmData.$.InResponseTo;
if (subjectInResponseTo) {
// * If the source idp encoded ~ as _x007E_ then swap it back, they could be also incorrectly encoding other values, but it doesn't seem like there is a standard on this.
if (subjectInResponseTo != inResponseTo && subjectInResponseTo?.replace(/_x007E_/gi, '~') !== inResponseTo?.replace(/_x007E_/gi, '~')) {
const error = new Error("InResponseTo is not valid");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2339
error.code = 'InvalidAuthenticationRequestId';
throw error;
}
}
}
const conditions = assertion.Conditions ? assertion.Conditions[0] : null;
if (assertion.Conditions && assertion.Conditions.length > 1) {
throw new Error("Unable to process multiple conditions in SAML assertion");
}
if (conditions && conditions.$) {
const maxTimeLimitMs = this.processMaxAgeAssertionTime(this.requestIdExpirationPeriodMs, conditions.$.NotOnOrAfter, assertion.$.IssueInstant);
const conErr = this.checkTimestampsValidityError(conditions.$.NotBefore, conditions.$.NotOnOrAfter, maxTimeLimitMs);
if (conErr)
throw conErr;
}
const audienceErr = this.checkAudienceValidityError(applicationEntityId, 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) {
const profileAttributes = {};
attributes.forEach((attribute) => {
if (!Object.prototype.hasOwnProperty.call(attribute, "AttributeValue")) {
// if attributes has no AttributeValue child, continue
return;
}
const name = attribute.$.Name;
const value = attribute.AttributeValue.length === 1
? attrValueMapper(attribute.AttributeValue[0])
: attribute.AttributeValue.map(attrValueMapper);
profileAttributes[name] = value;
// If any property is already present in profile and is also present
// in attributes, then skip the one from attributes. Handle this
// conflict gracefully without returning any error
if (Object.prototype.hasOwnProperty.call(profile, name)) {
return;
}
profile[name] = value;
});
profile.attributes = profileAttributes;
}
}
if (!profile.email && profile.mail) {
profile.email = profile.mail;
}
if (!profile.email && 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.email = profile["urn:oid:0.9.2342.19200300.100.1.3"];
}
return { profile, loggedOut: false };
}
checkTimestampsValidityError(notBefore, notOnOrAfter, maxTimeLimitMs, acceptedClockSkewMs = -1) {
if (acceptedClockSkewMs == -1)
return null;
const nowMs = new Date().getTime();
if (notBefore) {
const notBeforeMs = (0, datetime_1.dateStringToTimestamp)(notBefore, "NotBefore");
if (nowMs + acceptedClockSkewMs < notBeforeMs)
return new Error("SAML assertion not yet valid");
}
if (notOnOrAfter) {
const notOnOrAfterMs = (0, datetime_1.dateStringToTimestamp)(notOnOrAfter, "NotOnOrAfter");
if (nowMs - acceptedClockSkewMs >= notOnOrAfterMs)
return new Error("SAML assertion expired: clocks skewed too much");
}
if (maxTimeLimitMs) {
if (nowMs - acceptedClockSkewMs >= maxTimeLimitMs)
return new Error("SAML assertion expired: assertion too old");
}
return null;
}
checkAudienceValidityError(expectedAudience, audienceRestrictions) {
if (new url_1.URL(expectedAudience).hostname === 'localhost' || new url_1.URL(expectedAudience).hostname === '127.0.0.1') {
return null;
}
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;
}
/**
* Process max age assertion and use it if it is more restrictive than the NotOnOrAfter age
* assertion received in the SAMLResponse.
*
* @param maxAssertionAgeMs Max time after IssueInstant that we will accept assertion, in Ms.
* @param notOnOrAfter Expiration provided in response.
* @param issueInstant Time when response was issued.
* @returns {*} The expiration time to be used, in Ms.
*/
processMaxAgeAssertionTime(maxAssertionAgeMs, notOnOrAfter, issueInstant) {
const notOnOrAfterMs = (0, datetime_1.dateStringToTimestamp)(notOnOrAfter, "NotOnOrAfter");
const issueInstantMs = (0, datetime_1.dateStringToTimestamp)(issueInstant, "IssueInstant");
return Math.min(issueInstantMs + maxAssertionAgeMs, notOnOrAfterMs);
}
}
exports.default = SamlLogin;
//# sourceMappingURL=saml.js.map