[FIX] SAML issues (#11135)

[FIX] SAML issues
pull/11536/head
Pierre H. Lehnen 7 years ago committed by Diego Sampaio
parent 54c9e16ba1
commit 4802ed112d
  1. 5
      package-lock.json
  2. 2
      package.json
  3. 26
      packages/meteor-accounts-saml/README.md
  4. 350
      packages/meteor-accounts-saml/saml_utils.js

5
package-lock.json generated

@ -665,6 +665,11 @@
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg="
},
"arraybuffer-to-string": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/arraybuffer-to-string/-/arraybuffer-to-string-1.0.2.tgz",
"integrity": "sha512-WbIYlLVmvIAyUBdQRRuyGOJRriOQy9OAsWcyURmsRQp9+g647hdMSS2VFKXbJLVw0daUu06hqwLXm9etVrXI9A=="
},
"arrify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",

@ -117,6 +117,7 @@
"@slack/client": "^4.2.2",
"adm-zip": "^0.4.11",
"archiver": "^2.1.1",
"arraybuffer-to-string": "^1.0.2",
"atlassian-crowd": "^0.5.0",
"autolinker": "^1.6.2",
"aws-sdk": "^2.250.1",
@ -180,7 +181,6 @@
"webdav": "^1.5.2",
"wolfy87-eventemitter": "^5.2.4",
"xml-crypto": "^0.10.1",
"xml2js": "^0.4.19",
"xmlbuilder": "^10.0.0",
"xmldom": "^0.1.27",
"yaqrcode": "^0.2.1"

@ -29,28 +29,33 @@ settings = {"saml":[{
"cert":"MIICizCCAfQCCQCY8tKaMc0 LOTS OF FUNNY CHARS ==",
"idpSLORedirectURL": "http://openam.idp.io/openam/IDPSloRedirect/metaAlias/zimt/idp",
"privateKeyFile": "certs/mykey.pem", // path is relative to $METEOR-PROJECT/private
"publicCertFile": "certs/mycert.pem" // eg $METEOR-PROJECT/private/certs/mycert.pem
"publicCertFile": "certs/mycert.pem", // eg $METEOR-PROJECT/private/certs/mycert.pem
"dynamicProfile": true // set to true if we want to create a user in Meteor.users dynamically if SAML assertion is valid
"identifierFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", // Defaults to urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
"localProfileMatchAttribute": "telephoneNumber" // CAUTION: this will be mapped to profile.<localProfileMatchAttribute> attribute in Mongo if identifierFormat (see above) differs from urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress,
"attributesSAML": [telephoneNumber, sn, givenName, mail], // attrs from SAML attr statement, which will be used for local Meteor profile creation. Currently no real attribute mapping. If required use mapping on IdP side.
}]}
Meteor.settings = settings;
```
in some template
```
<a class="saml-login" data-provider="openam">OpenAM</a>
<a href="#" class="saml-login" data-provider="openam">OpenAM</a>
```
in helper function
```
'click .saml-login': function(event, template){
'click .saml-login' (event) {
event.preventDefault();
var provider = $(event.target).data('provider');
var provider = event.target.getAttribute('data-provider');
Meteor.loginWithSaml({
provider:provider
}, function(error, result){
//handle errors and result
provider
}, function(error, result) {
//handle errors and result
});
}
```
@ -73,7 +78,7 @@ and if SingleLogout is needed
1. Create a Meteor project by `meteor create sp` and cd into it.
2. Add `steffo:meteor-accounts-saml`
3. Create `server/lib/settings.js` as described above. Since Meteor loads things in `server/lib` first, this ensures that your settings are respected even on Galaxy where you cannot use `meteor --settings`.
3. Create `server/lib/settings.js` as described above. Since Meteor loads things in `server/lib` first, this ensures that your settings are respected even on Galaxy where you cannot use `meteor --settings`.
4. Put your private key and your cert (not the IDP's one) into the "private" directory. Eg if your meteor project is at `/Users/steffo/sp` then place them in `/Users/steffo/sp/private`
5. Check if you can receive SP metadata eg via `curl http://localhost:3000/_saml/metadata/openam`. Output should look like:
@ -103,7 +108,7 @@ and if SingleLogout is needed
1. I prefer using OpenAM realms. Set up a realm using a name that matches the one in the entry point URL of the `settings.json` file: `https://openam.idp.io/openam/SSORedirect/metaAlias/<YOURREALM>/idp`; we used `zimt` above.
2. Save the SP metadata (obtained in Step 5 above) in a file `sp-metadata.xml`.
3. Logon OpenSSO console as `amadmin` and select _Common Tasks > Register Remote Service Provider_
4. Select the corresponding real and upload the metadata (alternatively, point OpenAM to the SP's metadata URL eg `http://sp.meteor.com/_saml/metadata/openam`). If all goes well the new SP shows up under _Federation > Entity Providers_
4. Select the corresponding real and upload the metadata (alternatively, point OpenAM to the SP's metadata URL eg `http://sp.meteor.com/_saml/metadata/openam`). If all goes well the new SP shows up under _Federation > Entity Providers_
@ -120,4 +125,3 @@ and if SingleLogout is needed
## Credits
Based Nat Strauser's Meteor/SAML package _natestrauser:meteor-accounts-saml_ which is
heavily derived from https://github.com/bergie/passport-saml.

@ -1,12 +1,12 @@
/* globals SAML:true */
import zlib from 'zlib';
import xml2js from 'xml2js';
import xmlCrypto from 'xml-crypto';
import crypto from 'crypto';
import xmldom from 'xmldom';
import querystring from 'querystring';
import xmlbuilder from 'xmlbuilder';
import array2string from 'arraybuffer-to-string';
// var prefixMatch = new RegExp(/(?!xmlns)^.*:/);
@ -15,6 +15,12 @@ SAML = function(options) {
this.options = this.initialize(options);
};
function debugLog() {
if (Meteor.settings.debug) {
console.log.apply(this, arguments);
}
}
// var stripPrefix = function(str) {
// return str.replace(prefixMatch, '');
// };
@ -83,14 +89,12 @@ SAML.prototype.generateAuthorizeRequest = function(req) {
}
let request =
`<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="${ id }" Version="2.0" IssueInstant="${ instant
}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="${ callbackUrl }" Destination="${
`<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" AssertionConsumerServiceURL="${ callbackUrl }" Destination="${
this.options.entryPoint }">` +
`<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${ this.options.issuer }</saml:Issuer>\n`;
if (this.options.identifierFormat) {
request += `<samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Format="${ this.options.identifierFormat
}" AllowCreate="true"></samlp:NameIDPolicy>\n`;
request += `<samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Format="${ this.options.identifierFormat }" AllowCreate="true"></samlp:NameIDPolicy>\n`;
}
request +=
@ -111,8 +115,7 @@ SAML.prototype.generateLogoutRequest = function(options) {
const instant = this.generateInstant();
let request = `${ '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' +
'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="' }${ id }" Version="2.0" IssueInstant="${ instant
}" Destination="${ this.options.idpSLORedirectURL }">` +
'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="' }${ id }" Version="2.0" IssueInstant="${ instant }" Destination="${ this.options.idpSLORedirectURL }">` +
`<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${ this.options.issuer }</saml:Issuer>` +
`<saml:NameID Format="${ this.options.identifierFormat }">${ options.nameID }</saml:NameID>` +
'</samlp:LogoutRequest>';
@ -131,10 +134,10 @@ SAML.prototype.generateLogoutRequest = function(options) {
options.nameID }</saml:NameID>` +
`<samlp:SessionIndex xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">${ options.sessionIndex }</samlp:SessionIndex>` +
'</samlp:LogoutRequest>';
if (Meteor.settings.debug) {
console.log('------- SAML Logout request -----------');
console.log(request);
}
debugLog('------- SAML Logout request -----------');
debugLog(request);
return {
request,
id
@ -184,9 +187,8 @@ SAML.prototype.requestToUrl = function(request, operation, callback) {
target += querystring.stringify(samlRequest);
if (Meteor.settings.debug) {
console.log(`requestToUrl: ${ target }`);
}
debugLog(`requestToUrl: ${ target }`);
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
return callback(null, target);
@ -227,6 +229,34 @@ SAML.prototype.certToPEM = function(cert) {
// return res;
// }
SAML.prototype.validateStatus = function(doc) {
let successStatus = false;
let status = '';
let messageText = '';
const statusNodes = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusCode');
if (statusNodes.length) {
const statusNode = statusNodes[0];
const statusMessage = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage')[0];
if (statusMessage) {
messageText = statusMessage.firstChild.textContent;
}
status = statusNode.getAttribute('Value');
if (status === 'urn:oasis:names:tc:SAML:2.0:status:Success') {
successStatus = true;
}
}
return {
success: successStatus,
message: messageText,
statusCode: status
};
};
SAML.prototype.validateSignature = function(xml, cert) {
const self = this;
@ -249,200 +279,198 @@ SAML.prototype.validateSignature = function(xml, cert) {
return sig.checkSignature(xml);
};
SAML.prototype.getElement = function(parentElement, elementName) {
if (parentElement[`saml:${ elementName }`]) {
return parentElement[`saml:${ elementName }`];
} else if (parentElement[`samlp:${ elementName }`]) {
return parentElement[`samlp:${ elementName }`];
} else if (parentElement[`saml2p:${ elementName }`]) {
return parentElement[`saml2p:${ elementName }`];
} else if (parentElement[`saml2:${ elementName }`]) {
return parentElement[`saml2:${ elementName }`];
} else if (parentElement[`ns0:${ elementName }`]) {
return parentElement[`ns0:${ elementName }`];
} else if (parentElement[`ns1:${ elementName }`]) {
return parentElement[`ns1:${ elementName }`];
}
return parentElement[elementName];
};
SAML.prototype.validateLogoutResponse = function(samlResponse, callback) {
const self = this;
const compressedSAMLResponse = new Buffer(samlResponse, 'base64');
zlib.inflateRaw(compressedSAMLResponse, function(err, decoded) {
if (err) {
if (Meteor.settings.debug) {
console.log(err);
}
debugLog(`Error while inflating. ${ err }`);
} else {
const parser = new xml2js.Parser({
explicitRoot: true
});
parser.parseString(decoded, function(err, doc) {
const response = self.getElement(doc, 'LogoutResponse');
console.log(`constructing new DOM parser: ${ Object.prototype.toString.call(decoded) }`);
console.log(`>>>> ${ decoded }`);
const doc = new xmldom.DOMParser().parseFromString(array2string(decoded), 'text/xml');
if (doc) {
const response = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'LogoutResponse')[0];
if (response) {
// TBD. Check if this msg corresponds to one we sent
const inResponseTo = response.$.InResponseTo;
if (Meteor.settings.debug) {
console.log(`In Response to: ${ inResponseTo }`);
}
const status = self.getElement(response, 'Status');
const statusCode = self.getElement(status[0], 'StatusCode')[0].$.Value;
if (Meteor.settings.debug) {
console.log(`StatusCode: ${ JSON.stringify(statusCode) }`);
let inResponseTo;
try {
inResponseTo = response.getAttribute('InResponseTo');
debugLog(`In Response to: ${ inResponseTo }`);
} catch (e) {
if (Meteor.settings.debug) {
console.log(`Caught error: ${ e }`);
const msg = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage');
console.log(`Unexpected msg from IDP. Does your session still exist at IDP? Idp returned: \n ${ msg }`);
}
}
if (statusCode === 'urn:oasis:names:tc:SAML:2.0:status:Success') {
// In case of a successful logout at IDP we return inResponseTo value.
// This is the only way how we can identify the Meteor user (as we don't use Session Cookies)
const statusValidateObj = self.validateStatus(doc);
if (statusValidateObj.success) {
callback(null, inResponseTo);
} else {
callback('Error. Logout not confirmed by IDP', null);
}
} else {
callback('No Response Found', null);
}
});
}
}
});
};
SAML.prototype.mapAttributes = function(attributeStatement, profile) {
debugLog(`Attribute Statement found in SAML response: ${ attributeStatement }`);
const attributes = attributeStatement.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Attribute');
debugLog(`Attributes will be processed: ${ attributes.length }`);
if (attributes) {
for (let i = 0; i < attributes.length; i++) {
const values = attributes[i].getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AttributeValue');
let value;
if (values.length === 1) {
value = values[0].textContent;
} else {
value = [];
for (let j=0;j<values.length;j++) {
value.push(values[j].textContent);
}
}
const key = attributes[i].getAttribute('Name');
debugLog(`Name: ${ attributes[i] }`);
debugLog(`Adding attribute from SAML response to profile: ${ key } = ${ value }`);
profile[key] = value;
}
} else {
debugLog('No Attributes found in SAML attribute statement.');
}
if (!profile.mail && profile['urn:oid:0.9.2342.19200300.100.1.3']) {
// See http://www.incommonfederation.org/attributesummary.html for definition of attribute OIDs
profile.mail = profile['urn:oid:0.9.2342.19200300.100.1.3'];
}
if (!profile.email && profile['urn:oid:1.2.840.113549.1.9.1']) {
profile.email = profile['urn:oid:1.2.840.113549.1.9.1'];
}
if (!profile.email && profile.mail) {
profile.email = profile.mail;
}
};
SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
const self = this;
const xml = new Buffer(samlResponse, 'base64').toString('utf8');
// We currently use RelayState to save SAML provider
if (Meteor.settings.debug) {
console.log(`Validating response with relay state: ${ xml }`);
}
const parser = new xml2js.Parser({
explicitRoot: true,
xmlns:true
});
debugLog(`Validating response with relay state: ${ xml }`);
parser.parseString(xml, function(err, doc) {
// Verify signature
if (Meteor.settings.debug) {
console.log('Verify signature');
}
if (self.options.cert && !self.validateSignature(xml, self.options.cert)) {
if (Meteor.settings.debug) {
console.log('Signature WRONG');
}
return callback(new Error('Invalid signature'), null, false);
}
if (Meteor.settings.debug) {
console.log('Signature OK');
}
const response = self.getElement(doc, 'Response');
if (Meteor.settings.debug) {
console.log('Got response');
}
if (response) {
const assertion = self.getElement(response, 'Assertion');
if (!assertion) {
return callback(new Error('Missing SAML assertion'), null, false);
}
const doc = new xmldom.DOMParser().parseFromString(xml, 'text/xml');
const profile = {};
if (doc) {
debugLog('Verify status');
const statusValidateObj = self.validateStatus(doc);
if (response.$ && response.$.InResponseTo) {
profile.inResponseToId = response.$.InResponseTo;
}
if (statusValidateObj.success) {
debugLog('Status ok');
const issuer = self.getElement(assertion[0], 'Issuer');
if (issuer) {
profile.issuer = issuer[0]._;
// 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);
}
debugLog('Signature OK');
const subject = self.getElement(assertion[0], 'Subject');
const response = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'Response')[0];
if (response) {
debugLog('Got response');
if (subject) {
const nameID = self.getElement(subject[0], 'NameID');
if (nameID) {
profile.nameID = nameID[0]._;
if (nameID[0].$.Format) {
profile.nameIDFormat = nameID[0].$.Format;
}
const assertion = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion')[0];
if (!assertion) {
return callback(new Error('Missing SAML assertion'), null, false);
}
}
const authnStatement = self.getElement(assertion[0], 'AuthnStatement');
const profile = {};
if (authnStatement) {
if (authnStatement[0].$.SessionIndex) {
if (response.hasAttribute('InResponseTo')) {
profile.inResponseToId = response.getAttribute('InResponseTo');
}
profile.sessionIndex = authnStatement[0].$.SessionIndex;
if (Meteor.settings.debug) {
console.log(`Session Index: ${ profile.sessionIndex }`);
}
} else if (Meteor.settings.debug) {
console.log('No Session Index Found');
const issuer = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Issuer')[0];
if (issuer) {
profile.issuer = issuer.textContent;
}
const subject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Subject')[0];
} else if (Meteor.settings.debug) {
console.log('No AuthN Statement found');
}
if (subject) {
const nameID = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'NameID')[0];
if (nameID) {
profile.nameID = nameID.textContent;
const attributeStatement = self.getElement(assertion[0], 'AttributeStatement');
if (attributeStatement) {
const attributes = self.getElement(attributeStatement[0], 'Attribute');
if (attributes) {
attributes.forEach(function(attribute) {
const value = self.getElement(attribute, 'AttributeValue');
const key = attribute.$.Name.value;
if (typeof value[0] === 'string') {
profile[key] = value[0];
} else {
profile[key] = value[0]._;
if (nameID.hasAttribute('Format')) {
profile.nameIDFormat = nameID.getAttribute('Format');
}
});
}
}
if (!profile.mail && profile['urn:oid:0.9.2342.19200300.100.1.3']) {
// See http://www.incommonfederation.org/attributesummary.html for definition of attribute OIDs
profile.mail = profile['urn:oid:0.9.2342.19200300.100.1.3'];
const authnStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AuthnStatement')[0];
if (authnStatement) {
if (authnStatement.hasAttribute('SessionIndex')) {
profile.sessionIndex = authnStatement.getAttribute('SessionIndex');
debugLog(`Session Index: ${ profile.sessionIndex }`);
} else {
debugLog('No Session Index Found');
}
} else {
debugLog('No AuthN Statement found');
}
if (!profile.email && profile.mail) {
profile.email = profile.mail;
const attributeStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AttributeStatement')[0];
if (attributeStatement) {
this.mapAttributes(attributeStatement, profile);
} else {
debugLog('No Attribute Statement found in SAML response.');
}
}
if (!profile.email && profile.nameID && (profile.nameIDFormat && profile.nameIDFormat.value != null ? profile.nameIDFormat.value : profile.nameIDFormat).indexOf('emailAddress') >= 0) {
profile.email = profile.nameID;
}
if (Meteor.settings.debug) {
console.log(`NameID: ${ JSON.stringify(profile) }`);
}
if (!profile.email && profile.nameID && profile.nameIDFormat && profile.nameIDFormat.indexOf('emailAddress') >= 0) {
profile.email = profile.nameID;
}
const profileKeys = Object.keys(profile);
for (let i = 0; i < profileKeys.length; i++) {
const key = profileKeys[i];
const profileKeys = Object.keys(profile);
for (let i = 0; i < profileKeys.length; i++) {
const key = profileKeys[i];
if (key.match(/\./)) {
profile[key.replace(/\./g, '-')] = profile[key];
delete profile[key];
if (key.match(/\./)) {
profile[key.replace(/\./g, '-')] = profile[key];
delete profile[key];
}
}
}
callback(null, profile, false);
} else {
const logoutResponse = self.getElement(doc, 'LogoutResponse');
if (logoutResponse) {
callback(null, null, true);
debugLog(`NameID: ${ JSON.stringify(profile) }`);
callback(null, profile, false);
} else {
return callback(new Error('Unknown SAML response message'), null, false);
}
const logoutResponse = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'LogoutResponse');
if (logoutResponse) {
callback(null, null, true);
} else {
return callback(new Error('Unknown SAML response message'), null, false);
}
}
} else {
return callback(new Error(`Status is: ${ statusValidateObj.statusCode }`), null, false);
}
});
}
};
let decryptionCert;
@ -498,22 +526,16 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) {
}
}
},
'#list': [
'EncryptionMethod': [
// this should be the set that the xmlenc library supports
{
'EncryptionMethod': {
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
}
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
},
{
'EncryptionMethod': {
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
}
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
},
{
'EncryptionMethod': {
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'
}
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'
}
]
};

Loading…
Cancel
Save