UNPKG

saml-login

Version:
561 lines 30.3 kB
"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