From 571214eea895672341bb173114aae792158c3c46 Mon Sep 17 00:00:00 2001 From: Richard Towers Date: Fri, 28 Apr 2017 15:24:59 +0100 Subject: [PATCH] Issue #206: Support signing AuthnRequests using the HTTP-POST Binding This commit adds support for signing AuthnRequests in the SAML HTTP-POST binding. In the POST Binding the signature sits inside the SAML message (as opposed to the Redirect binding, where the signture lives in the URL's query string). This will help suppport identity providers that require signed AuthnRequests over the HTTP-POST binding. Two new configuration options have been added: * `digestAlgorithm`: allows you to specify the digest algorithm for the signature. * `xmlSignatureTransforms`: allows you to configure which XML transforms to use. --- .gitignore | 1 + README.md | 3 +++ lib/passport-saml/algorithms.js | 34 +++++++++++++++++++++++ lib/passport-saml/saml-post-signing.js | 28 +++++++++++++++++++ lib/passport-saml/saml.js | 34 +++++++++++------------ test/saml-post-signing-tests.js | 37 ++++++++++++++++++++++++++ test/tests.js | 2 ++ 7 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 lib/passport-saml/algorithms.js create mode 100644 lib/passport-saml/saml-post-signing.js create mode 100644 test/saml-post-signing-tests.js diff --git a/.gitignore b/.gitignore index c2658d7d..c8fcd164 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +.vscode diff --git a/README.md b/README.md index 7ecee328..c269f27f 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ Config parameter details: * `privateCert`: see 'security and signatures' * `decryptionPvk`: optional private key that will be used to attempt to decrypt any encrypted assertions that are received * `signatureAlgorithm`: optionally set the signature algorithm for signing requests, valid values are 'sha1' (default), 'sha256', or 'sha512' + * `digestAlgorithm`: optionally set the digest algorithm for signing requests, valid values are 'sha1' (default), 'sha256', or 'sha512' + * `xmlSignatureTransforms`: optionally set an array of signature transforms to be used in HTTP-POST signatures. By default this is + [ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#' ] * Additional SAML behaviors * `additionalParams`: dictionary of additional query params to add to all requests * `additionalAuthorizeParams`: dictionary of additional query params to add to 'authorize' requests diff --git a/lib/passport-saml/algorithms.js b/lib/passport-saml/algorithms.js new file mode 100644 index 00000000..cfb19d0a --- /dev/null +++ b/lib/passport-saml/algorithms.js @@ -0,0 +1,34 @@ +var crypto = require('crypto'); + +exports.getSigningAlgorithm = function getSigningAlgorithm (shortName) { + switch(shortName) { + case 'sha256': + return 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; + case 'sha512': + return 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'; + default: + return 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + } +}; + +exports.getDigestAlgorithm = function getDigestAlgorithm (shortName) { + switch(shortName) { + case 'sha256': + return 'http://www.w3.org/2001/04/xmlenc#sha256'; + case 'sha512': + return 'http://www.w3.org/2001/04/xmlenc#sha512'; + default: + return 'http://www.w3.org/2000/09/xmldsig#sha1'; + } +}; + +exports.getSigner = function getSigner (shortName) { + switch(shortName) { + case 'sha256': + return crypto.createSign('RSA-SHA256'); + case 'sha512': + return crypto.createSign('RSA-SHA512'); + default: + return crypto.createSign('RSA-SHA1'); + } +}; \ No newline at end of file diff --git a/lib/passport-saml/saml-post-signing.js b/lib/passport-saml/saml-post-signing.js new file mode 100644 index 00000000..66d1b0a7 --- /dev/null +++ b/lib/passport-saml/saml-post-signing.js @@ -0,0 +1,28 @@ +var SignedXml = require('xml-crypto').SignedXml; +var algorithms = require('./algorithms'); + +var authnRequestXPath = '/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]'; +var defaultTransforms = [ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#' ]; + +function signSamlPost(samlMessage, xpath, options) { + if (!samlMessage) throw new Error('samlMessage is required'); + if (!xpath) throw new Error('xpath is required'); + if (!options || !options.privateCert) throw new Error('options.privateCert is required'); + + var transforms = options.xmlSignatureTransforms || defaultTransforms; + var sig = new SignedXml(); + if (options.signatureAlgorithm) { + sig.signatureAlgorithm = algorithms.getSigningAlgorithm(options.signatureAlgorithm); + } + sig.addReference(xpath, transforms, algorithms.getDigestAlgorithm(options.digestAlgorithm)); + sig.signingKey = options.privateCert; + sig.computeSignature(samlMessage); + return sig.getSignedXml(); +} + +function signAuthnRequestPost(authnRequest, options) { + return signSamlPost(authnRequest, authnRequestXPath, options); +} + +exports.signSamlPost = signSamlPost; +exports.signAuthnRequestPost = signAuthnRequestPost; \ No newline at end of file diff --git a/lib/passport-saml/saml.js b/lib/passport-saml/saml.js index 42d4c208..42a48b88 100644 --- a/lib/passport-saml/saml.js +++ b/lib/passport-saml/saml.js @@ -9,6 +9,8 @@ var xmlbuilder = require('xmlbuilder'); var xmlenc = require('xml-encryption'); var xpath = xmlCrypto.xpath; var InMemoryCacheProvider = require('./inmemory-cache-provider.js').CacheProvider; +var algorithms = require('./algorithms'); +var signAuthnRequestPost = require('./saml-post-signing').signAuthnRequestPost; var Q = require('q'); var SAML = function (options) { @@ -109,20 +111,8 @@ SAML.prototype.generateInstant = function () { SAML.prototype.signRequest = function (samlMessage) { var signer; var samlMessageToSign = {}; - switch(this.options.signatureAlgorithm) { - case 'sha256': - samlMessage.SigAlg = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; - signer = crypto.createSign('RSA-SHA256'); - break; - case 'sha512': - samlMessage.SigAlg = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'; - signer = crypto.createSign('RSA-SHA512'); - break; - default: - samlMessage.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; - signer = crypto.createSign('RSA-SHA1'); - break; - } + samlMessage.SigAlg = algorithms.getSigningAlgorithm(this.options.signatureAlgorithm); + signer = algorithms.getSigner(this.options.signatureAlgorithm); if (samlMessage.SAMLRequest) { samlMessageToSign.SAMLRequest = samlMessage.SAMLRequest; } @@ -139,11 +129,13 @@ SAML.prototype.signRequest = function (samlMessage) { samlMessage.Signature = signer.sign(this.options.privateCert, 'base64'); }; -SAML.prototype.generateAuthorizeRequest = function (req, isPassive, callback) { +SAML.prototype.generateAuthorizeRequest = function (req, options, callback) { var self = this; var id = "_" + self.generateUniqueID(); var instant = self.generateInstant(); var forceAuthn = self.options.forceAuthn || false; + var isPassive = options.isPassive; + var isHttpPostBinding = options.isHttpPostBinding; Q.fcall(function() { if(self.options.validateInResponseTo) { @@ -199,7 +191,11 @@ SAML.prototype.generateAuthorizeRequest = function (req, isPassive, callback) { request['samlp:AuthnRequest']['@AttributeConsumingServiceIndex'] = self.options.attributeConsumingServiceIndex; } - callback(null, xmlbuilder.create(request).end()); + var stringRequest = xmlbuilder.create(request).end(); + if (isHttpPostBinding && self.options.privateCert) { + stringRequest = signAuthnRequestPost(stringRequest, self.options); + } + callback(null, stringRequest); }) .fail(function(err){ callback(err); @@ -353,7 +349,7 @@ SAML.prototype.getAdditionalParams = function (req, operation) { SAML.prototype.getAuthorizeUrl = function (req, callback) { var self = this; - self.generateAuthorizeRequest(req, self.options.passive, function(err, request){ + self.generateAuthorizeRequest(req, { isPassive: self.options.passive, isHttpPostBinding: false }, function(err, request) { if (err) return callback(err); var operation = 'authorize'; @@ -421,7 +417,7 @@ SAML.prototype.getAuthorizeForm = function (req, callback) { ].join('\r\n')); }; - self.generateAuthorizeRequest(req, self.options.passive, function(err, request) { + self.generateAuthorizeRequest(req, { isPassive: self.options.passive, isHttpPostBinding: true }, function(err, request) { if (err) { return callback(err); } @@ -923,7 +919,7 @@ SAML.prototype.generateServiceProviderMetadata = function( decryptionCert ) { { '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' }, { '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' } ] - } + }; } if (this.options.logoutCallbackUrl) { diff --git a/test/saml-post-signing-tests.js b/test/saml-post-signing-tests.js new file mode 100644 index 00000000..1ac25070 --- /dev/null +++ b/test/saml-post-signing-tests.js @@ -0,0 +1,37 @@ +const fs = require('fs'); +const should = require('should'); +const samlPostSigning = require('../lib/passport-saml/saml-post-signing'); +const signSamlPost = samlPostSigning.signSamlPost; +const signAuthnRequestPost = samlPostSigning.signAuthnRequestPost; + +const signingKey = fs.readFileSync(__dirname + '/static/key.pem'); + +describe('SAML POST Signing', function () { + it('should sign a simple saml request', function () { + var xml = ''; + var result = signSamlPost(xml, '/SAMLRequest', { privateCert: signingKey }); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/DigestValue>/); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/SignatureValue>/); + }); + + it('should sign and digest with SHA256 when specified', function () { + var xml = ''; + var options = { + signatureAlgorithm: 'sha256', + digestAlgorithm: 'sha256', + privateCert: signingKey + } + var result = signSamlPost(xml, '/SAMLRequest', options); + result.should.match(//); + result.should.match(//); + result.should.match(//); + }); + + it('should sign an AuthnRequest', function () { + var xml = ''; + var result = signAuthnRequestPost(xml, { privateCert: signingKey }); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/DigestValue>/); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/SignatureValue>/); + }); +}); diff --git a/test/tests.js b/test/tests.js index 9ee27aa9..ea656279 100644 --- a/test/tests.js +++ b/test/tests.js @@ -780,6 +780,7 @@ describe( 'passport-saml /', function() { var samlObj = new SAML( samlConfig ); samlObj.generateUniqueID = function () { return '12345678901234567890' }; samlObj.getAuthorizeUrl({}, function(err, url) { + if (err) { throw err; } var qry = require('querystring').parse(require('url').parse(url).query); qry.SigAlg.should.match('http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'); qry.Signature.should.match('SL85w0h6Pt7ejplGrR4OOTh4Zo9zs/MQHZep27kSzs4+U/0QdQi7hg5T0TKqCSRBZpVtspMpw+i6F0tZrFot0dIJgeCgkvMA2Tllwt6K0DbKWOiNXW5S2M9tUZktdJVfjr2D5e0SG4jQIwa4PVONgNQEKFxydIqwxVh9NGYeDeMUGq5/4QpMDLgYOvLfShyvhlzmqeUs7LBlZbKJLCeXZi/Z5bnF+QOAugtKuh0G6kFOS0CmKVLIW/4XicLHmggUBDlt0VJaskxUx2amHSNUoYe3Z9/9TeZqc7IswNUOEiq/oy0DLhokLnBEj+dBRMlgkAHp/gaWcc1Vp/1jSlVAvg=='); @@ -804,6 +805,7 @@ describe( 'passport-saml /', function() { var samlObj = new SAML( samlConfig ); samlObj.generateUniqueID = function () { return '12345678901234567890' }; samlObj.getAuthorizeUrl({}, function(err, url) { + if (err) { throw err; } var qry = require('querystring').parse(require('url').parse(url).query); qry.SigAlg.should.match('http://www.w3.org/2000/09/xmldsig#rsa-sha1'); qry.Signature.should.match('VnYOXVDiIaio+Vt8D2XXVwdyvwhDcdvgrQSkeq85G+MfU31yK9fvYEPFARK5pF1uJakMsYrKzVBv7HLCFcYuztpuIZloMFvFkado0MxFK4A/QFZn+EYDJE8ddLSvrW3iyuoxyVBSnH0+KLzDiI81B28YZNU3NFJIKCKzQSGIllJ7Vgw6KjH/BmE5DY0eSeUCEe6OygHgazjSrNIWQQjww5nSGIqAQl94OVanZtQBrYIUtik+d1lAhnginG0UnPccstenxEMAun2uMGp9hVqroWQvWRbX/xspRpjPOrIkvv63FzEgmRObXVNqpzDICJRUSlhTLdXAm2hb+ScYocO6EQ==');