Merge branch 'develop'

# Conflicts:
#	saml_utils.js
This commit is contained in:
Steffo Weber 2017-05-12 19:13:19 +02:00
commit 3c3b02e5ca
3 changed files with 529 additions and 420 deletions

View file

@ -1,14 +1,18 @@
accounts-base@1.2.16
accounts-base@1.2.17
allow-deny@1.0.5
babel-compiler@6.18.1
autoupdate@1.3.12
babel-compiler@6.18.2
babel-runtime@1.0.1
base64@1.0.10
binary-heap@1.0.10
blaze@2.3.2
blaze-tools@1.0.9
blaze-tools@1.0.10
boilerplate-generator@1.0.11
caching-compiler@1.1.9
caching-html-compiler@1.0.7
callback-hook@1.0.10
check@1.2.5
coffeescript@1.0.17
ddp@1.2.5
ddp-client@1.3.4
ddp-common@1.2.8
@ -16,37 +20,47 @@ ddp-rate-limiter@1.0.7
ddp-server@1.3.14
deps@1.0.12
diff-sequence@1.0.7
ecmascript@0.7.2
ecmascript@0.7.3
ecmascript-runtime@0.3.15
ejson@1.0.13
geojson-utils@1.0.10
html-tools@1.0.10
html-tools@1.0.11
htmljs@1.0.11
http@1.2.12
id-map@1.0.9
jquery@1.11.10
local-test:steffo:meteor-accounts-saml@0.0.8
localstorage@1.0.12
logging@1.1.17
meteor@1.6.1
minimongo@1.0.21
modules@0.8.1
minimongo@1.0.23
modules@0.8.2
modules-runtime@0.7.10
mongo@1.1.16
mongo@1.1.17
mongo-id@1.0.6
npm-mongo@2.2.24
observe-sequence@1.0.16
ordered-dict@1.0.9
practicalmeteor:chai@2.1.0_1
practicalmeteor:loglevel@1.2.0_2
practicalmeteor:mocha@2.4.5_2
practicalmeteor:mocha-core@0.1.4
practicalmeteor:sinon@1.14.1_2
promise@0.8.8
random@1.0.10
rate-limit@1.0.8
reactive-var@1.0.11
reload@1.1.11
retry@1.0.9
routepolicy@1.0.12
service-configuration@1.0.11
spacebars@1.0.12
spacebars-compiler@1.0.12
steffo:meteor-accounts-saml@0.0.3
tracker@1.1.2
spacebars-compiler@1.1.0
steffo:meteor-accounts-saml@0.0.8
templating@1.1.14
templating-tools@1.1.1
tmeasday:test-reporter-helpers@0.2.1
tracker@1.1.3
ui@1.0.11
underscore@1.0.10
url@1.1.0

View file

@ -1,12 +1,12 @@
Package.describe({
name:"steffo:meteor-accounts-saml",
summary: "SAML Login (SP) for Meteor. Works with OpenAM, OpenIDP and provides Single Logout.",
version: "0.0.3",
version: "0.0.8",
git: "https://github.com/steffow/meteor-accounts-saml.git"
});
Package.on_use(function (api) {
api.versionsFrom('1.1.0.2');
api.versionsFrom('1.4.4.1');
api.use(['routepolicy','webapp','underscore', 'service-configuration'], 'server');
api.use(['http','accounts-base','random'], ['client', 'server']);
@ -14,7 +14,32 @@ Package.on_use(function (api) {
api.add_files('saml_client.js', 'client');
});
Package.onTest((api) => {
// Sets up a dependency on this package.
api.use('steffo:meteor-accounts-saml');
// Use the Mocha test framework.
api.use('practicalmeteor:mocha@2.4.5_2');
});
Npm.depends({
"depd": "1.1.0",
"bytes": "2.5.0",
"content-type": "1.0.2",
"debug": "2.6.3",
"ms": "1.0.0",
"http-errors": "1.6.1",
"inherits": "2.0.3",
"setprototypeof": "1.0.3",
"statuses": "1.3.1",
"iconv-lite": "0.4.15",
"on-finished": "2.3.0",
"ee-first": "1.1.1",
"qs": "6.4.0",
"raw-body": "2.2.0",
"unpipe": "1.0.0",
"type-is": "1.6.15",
"media-typer": "0.3.0",
"mime-types": "2.1.15",
"xml2js": "0.4.17",
"body-parser": "1.17.1",
"sax": "1.2.2",

View file

@ -1,447 +1,436 @@
var zlib = Npm.require('zlib');
var xml2js = Npm.require('xml2js');
var xmlCrypto = Npm.require('xml-crypto');
var crypto = Npm.require('crypto');
var xmldom = Npm.require('xmldom');
var querystring = Npm.require('querystring');
var xmlbuilder = Npm.require('xmlbuilder');
var xmlenc = Npm.require('xml-encryption');
var xpath = xmlCrypto.xpath;
var Dom = xmldom.DOMParser;
/* globals SAML:true */
'use strict';
var prefixMatch = new RegExp(/(?!xmlns)^.*:/);
const zlib = Npm.require('zlib');
const xml2js = Npm.require('xml2js');
const xmlCrypto = Npm.require('xml-crypto');
const crypto = Npm.require('crypto');
const xmldom = Npm.require('xmldom');
const querystring = Npm.require('querystring');
const xmlbuilder = Npm.require('xmlbuilder');
// var prefixMatch = new RegExp(/(?!xmlns)^.*:/);
SAML = function (options) {
this.options = this.initialize(options);
SAML = function(options) {
this.options = this.initialize(options);
};
var stripPrefix = function (str) {
return str.replace(prefixMatch, '');
// var stripPrefix = function(str) {
// return str.replace(prefixMatch, '');
// };
SAML.prototype.initialize = function(options) {
if (!options) {
options = {};
}
if (!options.protocol) {
options.protocol = 'https://';
}
if (!options.path) {
options.path = '/saml/consume';
}
if (!options.issuer) {
options.issuer = 'onelogin_saml';
}
if (options.identifierFormat === undefined) {
options.identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress';
}
if (options.authnContext === undefined) {
options.authnContext = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport';
}
return options;
};
SAML.prototype.initialize = function (options) {
if (!options) {
options = {};
}
if (!options.protocol) {
options.protocol = 'https://';
}
if (!options.path) {
options.path = '/saml/consume';
}
if (!options.issuer) {
options.issuer = 'onelogin_saml';
}
if (options.identifierFormat === undefined) {
options.identifierFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
}
if (options.authnContext === undefined) {
options.authnContext = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport";
}
return options;
SAML.prototype.generateUniqueID = function() {
const chars = 'abcdef0123456789';
let uniqueID = '';
for (let i = 0; i < 20; i++) {
uniqueID += chars.substr(Math.floor((Math.random() * 15)), 1);
}
return uniqueID;
};
SAML.prototype.generateUniqueID = function () {
var chars = "abcdef0123456789";
var uniqueID = "";
for (var i = 0; i < 20; i++) {
uniqueID += chars.substr(Math.floor((Math.random() * 15)), 1);
}
return uniqueID;
SAML.prototype.generateInstant = function() {
return new Date().toISOString();
};
SAML.prototype.generateInstant = function () {
var date = new Date();
return date.getUTCFullYear() + '-' + ('0' + (date.getUTCMonth() + 1)).slice(-2) + '-' + ('0' + date.getUTCDate()).slice(-2) + 'T' + ('0' + (date.getUTCHours() + 2)).slice(-2) + ":" + ('0' + date.getUTCMinutes()).slice(-2) + ":" + ('0' + date.getUTCSeconds()).slice(-2) + "Z";
SAML.prototype.signRequest = function(xml) {
const signer = crypto.createSign('RSA-SHA1');
signer.update(xml);
return signer.sign(this.options.privateKey, 'base64');
};
SAML.prototype.signRequest = function (xml) {
var signer = crypto.createSign('RSA-SHA1');
signer.update(xml);
return signer.sign(this.options.privateKey, 'base64');
}
SAML.prototype.generateAuthorizeRequest = function(req) {
let id = `_${ this.generateUniqueID() }`;
const instant = this.generateInstant();
// Post-auth destination
let callbackUrl;
if (this.options.callbackUrl) {
callbackUrl = this.options.callbackUrl;
} else {
callbackUrl = this.options.protocol + req.headers.host + this.options.path;
}
SAML.prototype.generateAuthorizeRequest = function (req) {
var id = "_" + this.generateUniqueID();
var instant = this.generateInstant();
if (this.options.id) {
id = this.options.id;
}
// Post-auth destination
if (this.options.callbackUrl) {
callbackUrl = this.options.callbackUrl;
} else {
var callbackUrl = this.options.protocol + req.headers.host + this.options.path;
}
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="${
this.options.entryPoint }">` +
`<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${ this.options.issuer }</saml:Issuer>\n`;
if (this.options.id)
id = this.options.id;
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`;
}
var 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=\"" +
this.options.entryPoint + "\">" +
"<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" + this.options.issuer + "</saml:Issuer>\n";
request +=
'<samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Comparison="exact">' +
'<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n' +
'</samlp:AuthnRequest>';
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:RequestedAuthnContext xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Comparison=\"exact\">" +
"<saml:AuthnContextClassRef xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n" +
"</samlp:AuthnRequest>";
return request;
return request;
};
SAML.prototype.generateLogoutRequest = function (options) {
// options should be of the form
// nameId: <nameId as submitted during SAML SSO>
// sessionIndex: sessionIndex
// --- NO SAMLsettings: <Meteor.setting.saml entry for the provider you want to SLO from
SAML.prototype.generateLogoutRequest = function(options) {
// options should be of the form
// nameId: <nameId as submitted during SAML SSO>
// sessionIndex: sessionIndex
// --- NO SAMLsettings: <Meteor.setting.saml entry for the provider you want to SLO from
var id = "_" + this.generateUniqueID();
var instant = this.generateInstant();
const id = `_${ this.generateUniqueID() }`;
const instant = this.generateInstant();
var 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 + "\">" +
"<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>";
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 }">` +
`<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>';
request = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
"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 xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
"NameQualifier=\"http://id.init8.net:8080/openam\" " +
"SPNameQualifier=\"" + this.options.issuer + "\" " +
"Format=\"" + this.options.identifierFormat + "\">" +
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);
}
return {
request: request,
id: id
};
}
SAML.prototype.requestToUrl = function (request, operation, callback) {
var self = this;
var result;
zlib.deflateRaw(request, function (err, buffer) {
if (err) {
return callback(err);
}
var base64 = buffer.toString('base64');
var target = self.options.entryPoint;
if (operation === 'logout') {
if (self.options.idpSLORedirectURL) {
target = self.options.idpSLORedirectURL;
}
}
if (target.indexOf('?') > 0)
target += '&';
else
target += '?';
var samlRequest = {
SAMLRequest: base64
};
if (self.options.privateCert) {
samlRequest.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
samlRequest.Signature = self.signRequest(querystring.stringify(samlRequest));
}
// TBD. We should really include a proper RelayState here
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
var relayState = Meteor.absoluteUrl();
} else {
var relayState = self.options.provider;
}
target += querystring.stringify(samlRequest) + "&RelayState=" + relayState;
if (Meteor.settings.debug) {
console.log("requestToUrl: " + target);
}
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
result = target;
return callback(null, target);
} else {
callback(null, target);
}
});
}
SAML.prototype.getAuthorizeUrl = function (req, callback) {
var request = this.generateAuthorizeRequest(req);
this.requestToUrl(request, 'authorize', callback);
request = `${ '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' +
'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 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ' +
'NameQualifier="http://id.init8.net:8080/openam" ' +
`SPNameQualifier="${ this.options.issuer }" ` +
`Format="${ this.options.identifierFormat }">${
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);
}
return {
request,
id
};
};
SAML.prototype.getLogoutUrl = function (req, callback) {
var request = this.generateLogoutRequest(req);
SAML.prototype.requestToUrl = function(request, operation, callback) {
const self = this;
zlib.deflateRaw(request, function(err, buffer) {
if (err) {
return callback(err);
}
this.requestToUrl(request, 'logout', callback);
}
const base64 = buffer.toString('base64');
let target = self.options.entryPoint;
SAML.prototype.certToPEM = function (cert) {
cert = cert.match(/.{1,64}/g).join('\n');
cert = "-----BEGIN CERTIFICATE-----\n" + cert;
cert = cert + "\n-----END CERTIFICATE-----\n";
return cert;
if (operation === 'logout') {
if (self.options.idpSLORedirectURL) {
target = self.options.idpSLORedirectURL;
}
}
if (target.indexOf('?') > 0) {
target += '&';
} else {
target += '?';
}
// TBD. We should really include a proper RelayState here
let relayState;
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
relayState = Meteor.absoluteUrl();
} else {
relayState = self.options.provider;
}
const samlRequest = {
SAMLRequest: base64,
RelayState: relayState
};
if (self.options.privateCert) {
samlRequest.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
samlRequest.Signature = self.signRequest(querystring.stringify(samlRequest));
}
target += querystring.stringify(samlRequest);
if (Meteor.settings.debug) {
console.log(`requestToUrl: ${ target }`);
}
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
return callback(null, target);
} else {
callback(null, target);
}
});
};
function findChilds(node, localName, namespace) {
var res = []
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i]
if (child.localName == localName && (child.namespaceURI == namespace || !namespace)) {
res.push(child)
}
}
return res
}
SAML.prototype.getAuthorizeUrl = function(req, callback) {
const request = this.generateAuthorizeRequest(req);
SAML.prototype.validateSignature = function (xml, cert) {
var self = this;
var doc = new xmldom.DOMParser().parseFromString(xml);
var signature = xmlCrypto.xpath(doc, "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];
var sig = new xmlCrypto.SignedXml();
sig.keyInfoProvider = {
getKeyInfo: function (key) {
return "<X509Data></X509Data>"
},
getKey: function (keyInfo) {
return self.certToPEM(cert);
}
};
sig.loadSignature(signature);
return sig.checkSignature(xml);
this.requestToUrl(request, 'authorize', callback);
};
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];
}
return parentElement[elementName];
}
SAML.prototype.getLogoutUrl = function(req, callback) {
const request = this.generateLogoutRequest(req);
SAML.prototype.validateLogoutResponse = function (samlResponse, callback) {
var self = this;
var compressedSAMLResponse = new Buffer(samlResponse, 'base64');
zlib.inflateRaw(compressedSAMLResponse, function (err, decoded) {
if (err) {
if (Meteor.settings.debug) {
console.log(err)
}
} else {
var parser = new xml2js.Parser({
explicitRoot: true
});
parser.parseString(decoded, function (err, doc) {
var response = self.getElement(doc, 'LogoutResponse');
if (response) {
// TBD. Check if this msg corresponds to one we sent
var inResponseTo = response['$'].InResponseTo;
if (Meteor.settings.debug) {
console.log("In Response to: " + inResponseTo);
}
var status = self.getElement(response, 'Status');
var statusCode = self.getElement(status[0], 'StatusCode')[0]['$'].Value;
if (Meteor.settings.debug) {
console.log("StatusCode: " + JSON.stringify(statusCode));
}
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)
callback(null, inResponseTo);
} else {
callback("Error. Logout not confirmed by IDP", null);
}
} else {
callback("No Response Found", null);
}
})
}
})
}
SAML.prototype.validateResponse = function (samlResponse, relayState, callback) {
var self = this;
var xml = new Buffer(samlResponse, 'base64').toString('ascii');
// We currently use RelayState to save SAML provider
if (Meteor.settings.debug) {
console.log("Validating response with relay state: " + xml);
}
var parser = new xml2js.Parser({
explicitRoot: true
});
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");
}
var response = self.getElement(doc, 'Response');
if (Meteor.settings.debug) {
console.log("Got response");
}
if (response) {
var assertion = self.getElement(response, 'Assertion');
if (!assertion) {
return callback(new Error('Missing SAML assertion'), null, false);
}
profile = {};
if (response['$'] && response['$']['InResponseTo']) {
profile.inResponseToId = response['$']['InResponseTo'];
}
var issuer = self.getElement(assertion[0], 'Issuer');
if (issuer) {
profile.issuer = issuer[0];
}
var subject = self.getElement(assertion[0], 'Subject');
if (subject) {
var nameID = self.getElement(subject[0], 'NameID');
if (nameID) {
profile.nameID = nameID[0]["_"];
if (nameID[0]['$'].Format) {
profile.nameIDFormat = nameID[0]['$'].Format;
}
}
}
var authnStatement = self.getElement(assertion[0], 'AuthnStatement');
if (authnStatement) {
if (authnStatement[0]['$'].SessionIndex) {
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");
}
}
} else {
if (Meteor.settings.debug) {
console.log("No AuthN Statement found");
}
}
var attributeStatement = self.getElement(assertion[0], 'AttributeStatement');
if (attributeStatement) {
var attributes = self.getElement(attributeStatement[0], 'Attribute');
if (attributes) {
attributes.forEach(function (attribute) {
var value = self.getElement(attribute, 'AttributeValue');
if (typeof value[0] === 'string') {
profile[attribute['$'].Name] = value[0];
} else {
profile[attribute['$'].Name] = value[0]['_'];
}
});
}
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.mail) {
profile.email = profile.mail;
}
}
if (!profile.email && profile.nameID && profile.nameIDFormat && profile.nameIDFormat.indexOf('emailAddress') >= 0) {
profile.email = profile.nameID;
}
if (Meteor.settings.debug) {
console.log("NameID: " + JSON.stringify(profile));
}
callback(null, profile, false);
} else {
var logoutResponse = self.getElement(doc, 'LogoutResponse');
if (logoutResponse) {
callback(null, null, true);
} else {
return callback(new Error('Unknown SAML response message'), null, false);
}
}
});
this.requestToUrl(request, 'logout', callback);
};
SAML.prototype.certToPEM = function(cert) {
cert = cert.match(/.{1,64}/g).join('\n');
cert = `-----BEGIN CERTIFICATE-----\n${ cert }`;
cert = `${ cert }\n-----END CERTIFICATE-----\n`;
return cert;
};
// functionfindChilds(node, localName, namespace) {
// var res = [];
// for (var i = 0; i < node.childNodes.length; i++) {
// var child = node.childNodes[i];
// if (child.localName === localName && (child.namespaceURI === namespace || !namespace)) {
// res.push(child);
// }
// }
// return res;
// }
SAML.prototype.validateSignature = function(xml, cert) {
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 = {
getKeyInfo(/*key*/) {
return '<X509Data></X509Data>';
},
getKey(/*keyInfo*/) {
return self.certToPEM(cert);
}
};
sig.loadSignature(signature);
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 }`];
}
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);
}
} else {
const parser = new xml2js.Parser({
explicitRoot: true
});
parser.parseString(decoded, function(err, doc) {
const response = self.getElement(doc, 'LogoutResponse');
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) }`);
}
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)
callback(null, inResponseTo);
} else {
callback('Error. Logout not confirmed by IDP', null);
}
} else {
callback('No Response Found', null);
}
});
}
});
};
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
});
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 profile = {};
if (response.$ && response.$.InResponseTo) {
profile.inResponseToId = response.$.InResponseTo;
}
const issuer = self.getElement(assertion[0], 'Issuer');
if (issuer) {
profile.issuer = issuer[0]._;
}
const subject = self.getElement(assertion[0], 'Subject');
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 authnStatement = self.getElement(assertion[0], 'AuthnStatement');
if (authnStatement) {
if (authnStatement[0].$.SessionIndex) {
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');
}
} else if (Meteor.settings.debug) {
console.log('No AuthN Statement found');
}
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');
if (typeof value[0] === 'string') {
profile[attribute.$.Name] = value[0];
} else {
profile[attribute.$.Name] = value[0]._;
}
});
}
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.mail) {
profile.email = profile.mail;
}
}
if (!profile.email && profile.nameID && profile.nameIDFormat && profile.nameIDFormat.indexOf('emailAddress') >= 0) {
profile.email = profile.nameID;
}
if (Meteor.settings.debug) {
console.log(`NameID: ${ JSON.stringify(profile) }`);
}
callback(null, profile, false);
} else {
const logoutResponse = self.getElement(doc, 'LogoutResponse');
if (logoutResponse) {
callback(null, null, true);
} else {
return callback(new Error('Unknown SAML response message'), null, false);
}
}
});
};
<<<<<<< HEAD
SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) {
if (!decryptionCert) {
@ -521,4 +510,85 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) {
indent: ' ',
newline: '\n'
});
=======
let decryptionCert;
SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) {
if (!decryptionCert) {
decryptionCert = this.options.privateCert;
}
if (!this.options.callbackUrl && !callbackUrl) {
throw new Error(
'Unable to generate service provider metadata when callbackUrl option is not set');
}
const metadata = {
'EntityDescriptor': {
'@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata',
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
'@entityID': this.options.issuer,
'SPSSODescriptor': {
'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
'SingleLogoutService': {
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`,
'@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`
},
'NameIDFormat': this.options.identifierFormat,
'AssertionConsumerService': {
'@index': '1',
'@isDefault': 'true',
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
'@Location': callbackUrl
}
}
}
};
if (this.options.privateKey) {
if (!decryptionCert) {
throw new Error(
'Missing decryptionCert while generating metadata for decrypting service provider');
}
decryptionCert = decryptionCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, '');
decryptionCert = decryptionCert.replace(/-+END CERTIFICATE-+\r?\n?/, '');
decryptionCert = decryptionCert.replace(/\r\n/g, '\n');
metadata['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'] = {
'ds:KeyInfo': {
'ds:X509Data': {
'ds:X509Certificate': {
'#text': decryptionCert
}
}
},
'#list': [
// this should be the set that the xmlenc library supports
{
'EncryptionMethod': {
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
}
},
{
'EncryptionMethod': {
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
}
},
{
'EncryptionMethod': {
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'
}
}
]
};
}
return xmlbuilder.create(metadata).end({
pretty: true,
indent: ' ',
newline: '\n'
});
>>>>>>> develop
};