From 09cc7b9641c9f2b8b379710f837f791681bfb3d7 Mon Sep 17 00:00:00 2001 From: "Barth, Chris" Date: Wed, 20 Jan 2016 09:14:04 -0500 Subject: [PATCH] Include support for run-time params to be included in the generated URLs --- README.md | 2 +- lib/passport-saml/saml.js | 22 +++-- lib/passport-saml/strategy.js | 8 +- test/samlTests.js | 177 +++++++++++++++++++++++++++------- test/tests.js | 72 +++++++++++++- 5 files changed, 233 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 491ad68e3..3864d81e0 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Config parameter details: * `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) or 'sha256' * Additional SAML behaviors - * `additionalParams`: dictionary of additional query params to add to all requests + * `additionalParams`: dictionary of additional query params to add to all requests; this can also be part of the options that are passed to `authenticate` for params to be added on a per-call basis * `additionalAuthorizeParams`: dictionary of additional query params to add to 'authorize' requests * `identifierFormat`: if truthy, name identifier format to request from identity provider (default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) * `acceptedClockSkewMs`: Time in milliseconds of skew that is acceptable between client and server when checking `OnBefore` and `NotOnOrAfter` assertion condition validity timestamps. Setting to `-1` will disable checking these conditions entirely. Default is `0`. diff --git a/lib/passport-saml/saml.js b/lib/passport-saml/saml.js index 2ef65955f..2749fadc6 100644 --- a/lib/passport-saml/saml.js +++ b/lib/passport-saml/saml.js @@ -319,7 +319,7 @@ SAML.prototype.requestToUrl = function (request, response, operation, additional } }; -SAML.prototype.getAdditionalParams = function (req, operation) { +SAML.prototype.getAdditionalParams = function (req, operation, overrideParams) { var additionalParams = {}; var RelayState = req.query && req.query.RelayState || req.body && req.body.RelayState; @@ -344,16 +344,22 @@ SAML.prototype.getAdditionalParams = function (req, operation) { additionalParams[k] = optionsAdditionalParamsForThisOperation[k]; }); + overrideParams = overrideParams || {}; + Object.keys(overrideParams).forEach(function(k) { + additionalParams[k] = overrideParams[k]; + }); + return additionalParams; }; -SAML.prototype.getAuthorizeUrl = function (req, callback) { +SAML.prototype.getAuthorizeUrl = function (req, options, callback) { var self = this; self.generateAuthorizeRequest(req, self.options.passive, function(err, request){ if (err) return callback(err); var operation = 'authorize'; - self.requestToUrl(request, null, operation, self.getAdditionalParams(req, operation), callback); + var overrideParams = options ? options.additionalParams || {} : {}; + self.requestToUrl(request, null, operation, self.getAdditionalParams(req, operation, overrideParams), callback); }); }; @@ -431,16 +437,18 @@ SAML.prototype.getAuthorizeForm = function (req, callback) { }; -SAML.prototype.getLogoutUrl = function(req, callback) { +SAML.prototype.getLogoutUrl = function(req, options, callback) { var request = this.generateLogoutRequest(req); var operation = 'logout'; - this.requestToUrl(request, null, operation, this.getAdditionalParams(req, operation), callback); + var overrideParams = options ? options.additionalParams || {} : {}; + this.requestToUrl(request, null, operation, this.getAdditionalParams(req, operation, overrideParams), callback); }; -SAML.prototype.getLogoutResponseUrl = function(req, callback) { +SAML.prototype.getLogoutResponseUrl = function(req, options, callback) { var response = this.generateLogoutResponse(req, req.samlLogoutRequest); var operation = 'logout'; - this.requestToUrl(null, response, operation, this.getAdditionalParams(req, operation), callback); + var overrideParams = options ? options.additionalParams || {} : {}; + this.requestToUrl(null, response, operation, this.getAdditionalParams(req, operation, overrideParams), callback); }; SAML.prototype.certToPEM = function (cert) { diff --git a/lib/passport-saml/strategy.js b/lib/passport-saml/strategy.js index 869ebf6e7..180b2b601 100644 --- a/lib/passport-saml/strategy.js +++ b/lib/passport-saml/strategy.js @@ -38,7 +38,7 @@ Strategy.prototype.authenticate = function (req, options) { req.logout(); if (profile) { req.samlLogoutRequest = profile; - return self._saml.getLogoutResponseUrl(req, redirectIfSuccess); + return self._saml.getLogoutResponseUrl(req, options, redirectIfSuccess); } return self.pass(); } @@ -87,11 +87,11 @@ Strategy.prototype.authenticate = function (req, options) { } }); } else { // Defaults to HTTP-Redirect - this._saml.getAuthorizeUrl(req, redirectIfSuccess); + this._saml.getAuthorizeUrl(req, options, redirectIfSuccess); } }.bind(self), 'logout-request': function() { - this._saml.getLogoutUrl(req, redirectIfSuccess); + this._saml.getLogoutUrl(req, options, redirectIfSuccess); }.bind(self) }[options.samlFallback]; @@ -104,7 +104,7 @@ Strategy.prototype.authenticate = function (req, options) { }; Strategy.prototype.logout = function(req, callback) { - this._saml.getLogoutUrl(req, callback); + this._saml.getLogoutUrl(req, {}, callback); }; Strategy.prototype.generateServiceProviderMetadata = function( decryptionCert ) { diff --git a/test/samlTests.js b/test/samlTests.js index 9a6ae158c..75b3b14d3 100644 --- a/test/samlTests.js +++ b/test/samlTests.js @@ -4,49 +4,160 @@ var SAML = require('../lib/passport-saml/saml.js').SAML; var should = require('should'); var url = require('url'); -describe('SAML.js', function() { - describe('getAuthorizeUrl', function() { - var saml, req; - beforeEach(function() { +describe('SAML.js', function () { + describe('get Urls', function () { + var saml, req, options; + beforeEach(function () { saml = new SAML({ - entryPoint: 'https://exampleidp.com/path?key=value' + entryPoint: 'https://exampleidp.com/path?key=value', + logoutUrl: 'https://exampleidp.com/path?key=value' }); req = { protocol: 'https', headers: { host: 'examplesp.com' + }, + user: { + nameIDFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + nameID: 'nameID' + }, + samlLogoutRequest: { + ID: 123 } - } + }; + options = { + additionalParams: { + additionalKey: 'additionalValue' + } + }; }); - it('calls callback with right host', function(done) { - saml.getAuthorizeUrl(req, function(err, target) { - url.parse(target).host.should.equal('exampleidp.com'); - done(); + + describe('getAuthorizeUrl', function () { + it('calls callback with right host', function (done) { + saml.getAuthorizeUrl(req, {}, function (err, target) { + url.parse(target).host.should.equal('exampleidp.com'); + done(); + }); + }); + it('calls callback with right protocol', function (done) { + saml.getAuthorizeUrl(req, {}, function (err, target) { + url.parse(target).protocol.should.equal('https:'); + done(); + }); + }); + it('calls callback with right path', function (done) { + saml.getAuthorizeUrl(req, {}, function (err, target) { + url.parse(target).pathname.should.equal('/path'); + done(); + }); + }); + it('calls callback with original query string', function (done) { + saml.getAuthorizeUrl(req, {}, function (err, target) { + url.parse(target, true).query['key'].should.equal('value'); + done(); + }); + }); + it('calls callback with additional run-time params in query string', function (done) { + saml.getAuthorizeUrl(req, options, function (err, target) { + Object.keys(url.parse(target, true).query).should.have.length(3); + url.parse(target, true).query['key'].should.equal('value'); + url.parse(target, true).query['SAMLRequest'].should.not.be.empty(); + url.parse(target, true).query['additionalKey'].should.equal('additionalValue'); + done(); + }); + }); + // NOTE: This test only tests existence of the assertion, not the correctness + it('calls callback with saml request object', function (done) { + saml.getAuthorizeUrl(req, {}, function (err, target) { + url.parse(target, true).query.should.have.property('SAMLRequest'); + done(); + }); }); }); - it('calls callback with right protocol', function(done) { - saml.getAuthorizeUrl(req, function(err, target) { - url.parse(target).protocol.should.equal('https:'); - done(); - }); - }) - it('calls callback with right path', function(done) { - saml.getAuthorizeUrl(req, function(err, target) { - url.parse(target).pathname.should.equal('/path'); - done(); - }); - }) - it('calls callback with original query string', function(done) { - saml.getAuthorizeUrl(req, function(err, target) { - url.parse(target, true).query['key'].should.equal('value'); - done(); - }); - }) - // NOTE: This test only tests existence of the assertion, not the correctness - it('calls callback with saml request object', function(done) { - saml.getAuthorizeUrl(req, function(err, target) { - url.parse(target, true).query.should.have.property('SAMLRequest'); - done(); + + describe('getLogoutUrl', function () { + it('calls callback with right host', function (done) { + saml.getLogoutUrl(req, {}, function (err, target) { + url.parse(target).host.should.equal('exampleidp.com'); + done(); + }); + }); + it('calls callback with right protocol', function (done) { + saml.getLogoutUrl(req, {}, function (err, target) { + url.parse(target).protocol.should.equal('https:'); + done(); + }); + }); + it('calls callback with right path', function (done) { + saml.getLogoutUrl(req, {}, function (err, target) { + url.parse(target).pathname.should.equal('/path'); + done(); + }); + }); + it('calls callback with original query string', function (done) { + saml.getLogoutUrl(req, {}, function (err, target) { + url.parse(target, true).query['key'].should.equal('value'); + done(); + }); + }); + it('calls callback with additional run-time params in query string', function (done) { + saml.getLogoutUrl(req, options, function (err, target) { + Object.keys(url.parse(target, true).query).should.have.length(3); + url.parse(target, true).query['key'].should.equal('value'); + url.parse(target, true).query['SAMLRequest'].should.not.be.empty(); + url.parse(target, true).query['additionalKey'].should.equal('additionalValue'); + done(); + }); + }); + // NOTE: This test only tests existence of the assertion, not the correctness + it('calls callback with saml request object', function (done) { + saml.getLogoutUrl(req, {}, function (err, target) { + url.parse(target, true).query.should.have.property('SAMLRequest'); + done(); + }); + }); + }); + + describe('getLogoutResponseUrl', function () { + it('calls callback with right host', function (done) { + saml.getLogoutResponseUrl(req, {}, function (err, target) { + url.parse(target).host.should.equal('exampleidp.com'); + done(); + }); + }); + it('calls callback with right protocol', function (done) { + saml.getLogoutResponseUrl(req, {}, function (err, target) { + url.parse(target).protocol.should.equal('https:'); + done(); + }); + }); + it('calls callback with right path', function (done) { + saml.getLogoutResponseUrl(req, {}, function (err, target) { + url.parse(target).pathname.should.equal('/path'); + done(); + }); + }); + it('calls callback with original query string', function (done) { + saml.getLogoutResponseUrl(req, {}, function (err, target) { + url.parse(target, true).query['key'].should.equal('value'); + done(); + }); + }); + it('calls callback with additional run-time params in query string', function (done) { + saml.getLogoutResponseUrl(req, options, function (err, target) { + Object.keys(url.parse(target, true).query).should.have.length(3); + url.parse(target, true).query['key'].should.equal('value'); + url.parse(target, true).query['SAMLResponse'].should.not.be.empty(); + url.parse(target, true).query['additionalKey'].should.equal('additionalValue'); + done(); + }); + }); + // NOTE: This test only tests existence of the assertion, not the correctness + it('calls callback with saml response object', function (done) { + saml.getLogoutResponseUrl(req, {}, function (err, target) { + url.parse(target, true).query.should.have.property('SAMLResponse'); + done(); + }); }); }); }); diff --git a/test/tests.js b/test/tests.js index 4e5202951..cddfad59f 100644 --- a/test/tests.js +++ b/test/tests.js @@ -425,7 +425,7 @@ describe( 'passport-saml /', function() { encodedSamlRequest = querystring.parse( query ).SAMLRequest; } - var buffer = new Buffer(encodedSamlRequest, 'base64') + var buffer = new Buffer(encodedSamlRequest, 'base64'); if (check.config.skipRequestCompression) helper(null, buffer); else @@ -765,7 +765,7 @@ describe( 'passport-saml /', function() { }; var samlObj = new SAML( samlConfig ); samlObj.generateUniqueID = function () { return '12345678901234567890' }; - samlObj.getAuthorizeUrl({}, function(err, url) { + samlObj.getAuthorizeUrl({}, {}, function(err, url) { 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=='); @@ -789,7 +789,7 @@ describe( 'passport-saml /', function() { }; var samlObj = new SAML( samlConfig ); samlObj.generateUniqueID = function () { return '12345678901234567890' }; - samlObj.getAuthorizeUrl({}, function(err, url) { + samlObj.getAuthorizeUrl({}, {}, function(err, url) { 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=='); @@ -1019,6 +1019,41 @@ describe( 'passport-saml /', function() { done(); }); + it ( 'should merge run-time params additionalLogoutParams and additionalAuthorizeParams with additionalParams', function( done ) { + var samlConfig = { + entryPoint: 'https://app.onelogin.com/trust/saml2/http-post/sso/371755', + additionalParams: { + 'queryParam1': 'queryParamValue' + }, + additionalAuthorizeParams: { + 'queryParam2': 'queryParamValueAuthorize' + }, + additionalLogoutParams: { + 'queryParam2': 'queryParamValueLogout' + } + }; + var samlObj = new SAML( samlConfig ); + var options = { + additionalParams: { + queryParam3: 'queryParamRuntimeValue' + } + }; + + var additionalAuthorizeParams = samlObj.getAdditionalParams({}, 'authorize', options.additionalParams); + Object.keys(additionalAuthorizeParams).should.have.length(3); + additionalAuthorizeParams.should.containEql({'queryParam1': 'queryParamValue', + 'queryParam2': 'queryParamValueAuthorize', + 'queryParam3': 'queryParamRuntimeValue'}); + + var additionalLogoutParams = samlObj.getAdditionalParams({}, 'logout', options.additionalParams); + Object.keys(additionalLogoutParams).should.have.length(3); + additionalLogoutParams.should.containEql({'queryParam1': 'queryParamValue', + 'queryParam2': 'queryParamValueLogout', + 'queryParam3': 'queryParamRuntimeValue'}); + + done(); + }); + it ( 'should prioritize additionalLogoutParams and additionalAuthorizeParams over additionalParams', function( done ) { var samlConfig = { entryPoint: 'https://app.onelogin.com/trust/saml2/http-post/sso/371755', @@ -1044,6 +1079,37 @@ describe( 'passport-saml /', function() { done(); }); + + it ( 'should prioritize run-time params over all other params', function( done ) { + var samlConfig = { + entryPoint: 'https://app.onelogin.com/trust/saml2/http-post/sso/371755', + additionalParams: { + 'queryParam': 'queryParamValue' + }, + additionalAuthorizeParams: { + 'queryParam': 'queryParamValueAuthorize' + }, + additionalLogoutParams: { + 'queryParam': 'queryParamValueLogout' + } + }; + var samlObj = new SAML( samlConfig ); + var options = { + additionalParams: { + queryParam: 'queryParamRuntimeValue' + } + }; + + var additionalAuthorizeParams = samlObj.getAdditionalParams({}, 'authorize', options.additionalParams); + Object.keys(additionalAuthorizeParams).should.have.length(1); + additionalAuthorizeParams.should.containEql({'queryParam': 'queryParamRuntimeValue'}); + + var additionalLogoutParams = samlObj.getAdditionalParams({}, 'logout', options.additionalParams); + Object.keys(additionalLogoutParams).should.have.length(1); + additionalLogoutParams.should.containEql({'queryParam': 'queryParamRuntimeValue'}); + + done(); + }); }); describe( 'InResponseTo validation checks /', function(){