diff --git a/app/meteor-accounts-saml/server/saml_server.js b/app/meteor-accounts-saml/server/saml_server.js index 0b2c69bf16a1..15768ca68c34 100644 --- a/app/meteor-accounts-saml/server/saml_server.js +++ b/app/meteor-accounts-saml/server/saml_server.js @@ -229,6 +229,22 @@ const guessNameFromUsername = (username) => .replace(/^(.)/, (u) => u.toLowerCase()) .replace(/^\w/, (u) => u.toUpperCase()); +const findUser = (username, emailRegex) => { + if (Accounts.saml.settings.immutableProperty === 'Username') { + if (username) { + return Meteor.users.findOne({ + username, + }); + } + + return null; + } + + return Meteor.users.findOne({ + 'emails.address': emailRegex, + }); +}; + Accounts.registerLoginHandler(function(loginRequest) { if (!loginRequest.saml || !loginRequest.credentialToken) { return undefined; @@ -279,17 +295,7 @@ Accounts.registerLoginHandler(function(loginRequest) { // If eppn is not exist if (!user) { - if (Accounts.saml.settings.immutableProperty === 'Username') { - if (username) { - user = Meteor.users.findOne({ - username, - }); - } - } else { - user = Meteor.users.findOne({ - 'emails.address': emailRegex, - }); - } + user = findUser(username, emailRegex); } const emails = emailList.map((email) => ({ diff --git a/app/meteor-accounts-saml/server/saml_utils.js b/app/meteor-accounts-saml/server/saml_utils.js index ac3a3d1ea08b..5c68621d281b 100644 --- a/app/meteor-accounts-saml/server/saml_utils.js +++ b/app/meteor-accounts-saml/server/saml_utils.js @@ -311,12 +311,8 @@ SAML.prototype.validateStatus = function(doc) { }; }; -SAML.prototype.validateSignature = function(xml, cert) { +SAML.prototype.validateSignature = function(xml, cert, signature) { const self = this; - - const doc = new xmldom.DOMParser().parseFromString(xml); - const signature = xmlCrypto.xpath(doc, '//*[local-name(.)=\'Signature\' and namespace-uri(.)=\'http://www.w3.org/2000/09/xmldsig#\']')[0]; - const sig = new xmlCrypto.SignedXml(); sig.keyInfoProvider = { @@ -333,6 +329,35 @@ SAML.prototype.validateSignature = function(xml, cert) { return sig.checkSignature(xml); }; +SAML.prototype.validateSignatureChildren = function(xml, cert, parent) { + const xpathSigQuery = ".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']"; + const signatures = xmlCrypto.xpath(parent, xpathSigQuery); + let signature = null; + + for (const sign of signatures) { + if (sign.parentNode !== parent) { + continue; + } + + // Too many signatures + if (signature) { + return false; + } + + signature = sign; + } + + return this.validateSignature(xml, cert, signature); +}; + +SAML.prototype.validateResponseSignature = function(xml, cert, response) { + return this.validateSignatureChildren(xml, cert, response); +}; + +SAML.prototype.validateAssertionSignature = function(xml, cert, assertion) { + return this.validateSignatureChildren(xml, cert, assertion); +}; + SAML.prototype.validateLogoutRequest = function(samlRequest, callback) { const compressedSAMLRequest = new Buffer(samlRequest, 'base64'); zlib.inflateRaw(compressedSAMLRequest, function(err, decoded) { @@ -464,6 +489,23 @@ SAML.prototype.mapAttributes = function(attributeStatement, profile) { } }; +SAML.prototype.validateAssertionConditions = function(assertion) { + const conditions = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Conditions')[0]; + if (conditions && !this.validateNotBeforeNotOnOrAfterAssertions(conditions)) { + throw new Error('NotBefore / NotOnOrAfter assertion failed'); + } +}; + +SAML.prototype.validateSubjectConditions = function(subject) { + const subjectConfirmation = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmation')[0]; + if (subjectConfirmation) { + const subjectConfirmationData = subjectConfirmation.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmationData')[0]; + if (subjectConfirmationData && !this.validateNotBeforeNotOnOrAfterAssertions(subjectConfirmationData)) { + throw new Error('NotBefore / NotOnOrAfter assertion failed'); + } + } +}; + SAML.prototype.validateNotBeforeNotOnOrAfterAssertions = function(element) { const sysnow = new Date(); const allowedclockdrift = this.options.allowedClockDrift; @@ -491,6 +533,76 @@ SAML.prototype.validateNotBeforeNotOnOrAfterAssertions = function(element) { return true; }; +SAML.prototype.getAssertion = function(response) { + const allAssertions = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion'); + const allEncrypedAssertions = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedAssertion'); + + if (allAssertions.length + allEncrypedAssertions.length > 1) { + throw new Error('Too many SAML assertions'); + } + + let assertion = allAssertions[0]; + const encAssertion = allEncrypedAssertions[0]; + + + if (typeof encAssertion !== 'undefined') { + const options = { key: this.options.privateKey }; + xmlenc.decrypt(encAssertion.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) { + assertion = new xmldom.DOMParser().parseFromString(result, 'text/xml'); + }); + } + + if (!assertion) { + throw new Error('Missing SAML assertion'); + } + + return assertion; +}; + +SAML.prototype.verifySignatures = function(response, assertion, xml) { + if (!this.options.cert) { + return; + } + + debugLog('Verify Document Signature'); + if (!this.validateResponseSignature(xml, this.options.cert, response)) { + debugLog('Document Signature WRONG'); + throw new Error('Invalid Signature'); + } + debugLog('Document Signature OK'); + + debugLog('Verify Assertion Signature'); + if (!this.validateAssertionSignature(xml, this.options.cert, assertion)) { + debugLog('Assertion Signature WRONG'); + throw new Error('Invalid Assertion signature'); + } + debugLog('Assertion Signature OK'); +}; + +SAML.prototype.getSubject = function(assertion) { + let subject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Subject')[0]; + const encSubject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedID')[0]; + + if (typeof encSubject !== 'undefined') { + const options = { key: this.options.privateKey }; + xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) { + subject = new xmldom.DOMParser().parseFromString(result, 'text/xml'); + }); + } + + return subject; +}; + +SAML.prototype.getIssuer = function(assertion) { + const issuers = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Issuer'); + + if (issuers.length > 1) { + throw new Error('Too many Issuers'); + } + + return issuers[0]; +}; + SAML.prototype.validateResponse = function(samlResponse, relayState, callback) { const self = this; const xml = new Buffer(samlResponse, 'base64').toString('utf8'); @@ -510,15 +622,12 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) { } debugLog('Status ok'); - // Verify signature - debugLog('Verify signature'); - if (self.options.cert && !self.validateSignature(xml, self.options.cert)) { - debugLog('Signature WRONG'); - return callback(new Error('Invalid signature'), null, false); + const allResponses = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'Response'); + if (allResponses.length !== 1) { + return callback(new Error('Too many SAML responses'), null, false); } - debugLog('Signature OK'); - const response = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'Response')[0]; + const response = allResponses[0]; if (!response) { const logoutResponse = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'LogoutResponse'); @@ -529,19 +638,15 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) { } debugLog('Got response'); - let assertion = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion')[0]; - const encAssertion = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedAssertion')[0]; + let assertion; + let issuer; - const options = { key: this.options.privateKey }; + try { + assertion = this.getAssertion(response, callback); - if (typeof encAssertion !== 'undefined') { - xmlenc.decrypt(encAssertion.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) { - assertion = new xmldom.DOMParser().parseFromString(result, 'text/xml'); - }); - } - - if (!assertion) { - return callback(new Error('Missing SAML assertion'), null, false); + this.verifySignatures(response, assertion, xml); + } catch (e) { + return callback(e, null, false); } const profile = {}; @@ -550,19 +655,17 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) { profile.inResponseToId = response.getAttribute('InResponseTo'); } - const issuer = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Issuer')[0]; + try { + issuer = this.getIssuer(assertion); + } catch (e) { + return callback(e, null, false); + } + if (issuer) { profile.issuer = issuer.textContent; } - let subject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Subject')[0]; - const encSubject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedID')[0]; - - if (typeof encSubject !== 'undefined') { - xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) { - subject = new xmldom.DOMParser().parseFromString(result, 'text/xml'); - }); - } + const subject = this.getSubject(assertion); if (subject) { const nameID = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'NameID')[0]; @@ -574,18 +677,17 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) { } } - const subjectConfirmation = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmation')[0]; - if (subjectConfirmation) { - const subjectConfirmationData = subjectConfirmation.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmationData')[0]; - if (subjectConfirmationData && !this.validateNotBeforeNotOnOrAfterAssertions(subjectConfirmationData)) { - return callback(new Error('NotBefore / NotOnOrAfter assertion failed'), null, false); - } + try { + this.validateSubjectConditions(subject); + } catch (e) { + return callback(e, null, false); } } - const conditions = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Conditions')[0]; - if (conditions && !this.validateNotBeforeNotOnOrAfterAssertions(conditions)) { - return callback(new Error('NotBefore / NotOnOrAfter assertion failed'), null, false); + try { + this.validateAssertionConditions(assertion); + } catch (e) { + return callback(e, null, false); } const authnStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AuthnStatement')[0];