diff --git a/README.md b/README.md index 5d554087..1aca6d23 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ Passport-SAML has been tested to work with Onelogin, Okta, Shibboleth, [SimpleSA ## Usage -### Configure strategy +The examples utilize the [Feide OpenIdp identity provider](https://openidp.feide.no/). You need an account there to log in with this. You also need to [register your site](https://openidp.feide.no/simplesaml/module.php/metaedit/index.php) as a service provider. -This example utilizes the [Feide OpenIdp identity provider](https://openidp.feide.no/). You need an account there to log in with this. You also need to [register your site](https://openidp.feide.no/simplesaml/module.php/metaedit/index.php) as a service provider. +### Configure strategy The SAML identity provider will redirect you to the URL provided by the `path` configuration. @@ -43,6 +43,36 @@ passport.use(new SamlStrategy( ); ``` +### Configure strategy for multiple providers + +You can pass a `getSamlOptions` parameter to `MultiSamlStrategy` which will be called before the SAML flows. Passport-SAML will pass in the request object so you can decide which configuation is appropriate. + +```javascript +var MultiSamlStrategy = require('passport-saml/MultiSamlStrategy'); +[...] + +passport.use(new MultiSamlStrategy( + { + getSamlOptions: function(request, done) { + findProvider(request, function(err, provider) { + if (err) { + return done(err); + } + return done(null, provider.configuration); + }); + } + }, + function(profile, done) { + findByEmail(profile.email, function(err, user) { + if (err) { + return done(err); + } + return done(null, user); + }); + }) +); +``` + #### Config parameter details: * **Core** @@ -237,10 +267,6 @@ See [Releases](https://github.com/bergie/passport-saml/releases) to find the cha ## FAQ -### What if I have multiple SAML providers that my users may be connecting to? - -A single instance of passport-saml will only authenticate users against a single identity provider. If you have a use case where different logins need to be routed to different identity providers, you can create multiple instances of passport-saml, and either dispatch to them with your own routing code, or use a library like https://www.npmjs.org/package/passports. - ### Is there an example I can look at? Gerard Braad has provided an example app at https://github.com/gbraad/passport-saml-example/ diff --git a/multiSamlStrategy.js b/multiSamlStrategy.js new file mode 100644 index 00000000..f4ba1ad7 --- /dev/null +++ b/multiSamlStrategy.js @@ -0,0 +1,42 @@ +var util = require('util'); +var saml = require('./lib/passport-saml/saml'); +var SamlStrategy = require('./lib/passport-saml/strategy'); + +function MultiSamlStrategy (options, verify) { + if (!options || typeof options.getSamlOptions != 'function') { + throw new Error('Please provide a getSamlOptions function'); + } + + SamlStrategy.call(this, options, verify); + this._getSamlOptions = options.getSamlOptions; +} + +util.inherits(MultiSamlStrategy, SamlStrategy); + +MultiSamlStrategy.prototype.authenticate = function (req, options) { + var self = this; + + this._getSamlOptions(req, function (err, samlOptions) { + if (err) { + return self.error(err); + } + + self._saml = new saml.SAML(samlOptions); + self.constructor.super_.prototype.authenticate.call(self, req, options); + }); +}; + +MultiSamlStrategy.prototype.logout = function (req, options) { + var self = this; + + this._getSamlOptions(req, function (err, samlOptions) { + if (err) { + return self.error(err); + } + + self._saml = new saml.SAML(samlOptions); + self.constructor.super_.prototype.logout.call(self, req, options); + }); +}; + +module.exports = MultiSamlStrategy; diff --git a/test/multiSamlStrategy.js b/test/multiSamlStrategy.js new file mode 100644 index 00000000..4f0f1c11 --- /dev/null +++ b/test/multiSamlStrategy.js @@ -0,0 +1,151 @@ +'use strict'; + +var sinon = require('sinon'); +var should = require( 'should' ); +var SamlStrategy = require( '../lib/passport-saml/index.js' ).Strategy; +var MultiSamlStrategy = require( '../multiSamlStrategy' ); + +function verify () {} + +describe('Strategy()', function() { + it('extends passport Strategy', function() { + function getSamlOptions () { return {} } + var strategy = new MultiSamlStrategy({ getSamlOptions: getSamlOptions }, verify); + strategy.should.be.an.instanceOf(SamlStrategy); + }); + + it('throws if wrong finder is provided', function() { + function createStrategy (){ return new MultiSamlStrategy({}, verify) }; + should.throws(createStrategy); + }); +}); + +describe('strategy#authenticate', function() { + beforeEach(function() { + this.superAuthenticateStub = sinon.stub(SamlStrategy.prototype, 'authenticate'); + }); + + afterEach(function() { + this.superAuthenticateStub.restore(); + }); + + it('calls super with request and auth options', function(done) { + var superAuthenticateStub = this.superAuthenticateStub; + function getSamlOptions (req, fn) { + fn(); + sinon.assert.calledOnce(superAuthenticateStub); + done(); + }; + + var strategy = new MultiSamlStrategy({ getSamlOptions: getSamlOptions }, verify); + strategy.authenticate(); + }); + + it('passes options on to saml strategy', function(done) { + var passportOptions = { + passReqToCallback: true, + authnRequestBinding: 'HTTP-POST', + getSamlOptions: function (req, fn) { + fn(); + strategy._passReqToCallback.should.eql(true); + strategy._authnRequestBinding.should.eql('HTTP-POST'); + done(); + } + }; + + var strategy = new MultiSamlStrategy(passportOptions, verify); + strategy.authenticate(); + }); + + it('uses geted options to setup internal saml provider', function(done) { + var samlOptions = { + issuer: 'http://foo.issuer', + callbackUrl: 'http://foo.callback', + cert: 'deadbeef', + host: 'lvh', + acceptedClockSkewMs: -1, + identifierFormat: + 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + path: '/saml/callback', + logoutUrl: 'http://foo.slo', + signatureAlgorithm: 'sha256' + }; + + function getSamlOptions (req, fn) { + fn(null, samlOptions); + strategy._saml.options.should.containEql(samlOptions); + done(); + } + + var strategy = new MultiSamlStrategy( + { getSamlOptions: getSamlOptions }, + verify + ); + strategy.authenticate(); + }); +}); + +describe('strategy#logout', function() { + beforeEach(function() { + this.superAuthenticateStub = sinon.stub(SamlStrategy.prototype, 'logout'); + }); + + afterEach(function() { + this.superAuthenticateStub.restore(); + }); + + it('calls super with request and auth options', function(done) { + var superAuthenticateStub = this.superAuthenticateStub; + function getSamlOptions (req, fn) { + fn(); + sinon.assert.calledOnce(superAuthenticateStub); + done(); + }; + + var strategy = new MultiSamlStrategy({ getSamlOptions: getSamlOptions }, verify); + strategy.logout(); + }); + + it('passes options on to saml strategy', function(done) { + var passportOptions = { + passReqToCallback: true, + authnRequestBinding: 'HTTP-POST', + getSamlOptions: function (req, fn) { + fn(); + strategy._passReqToCallback.should.eql(true); + strategy._authnRequestBinding.should.eql('HTTP-POST'); + done(); + } + }; + + var strategy = new MultiSamlStrategy(passportOptions, verify); + strategy.logout(); + }); + + it('uses geted options to setup internal saml provider', function(done) { + var samlOptions = { + issuer: 'http://foo.issuer', + callbackUrl: 'http://foo.callback', + cert: 'deadbeef', + host: 'lvh', + acceptedClockSkewMs: -1, + identifierFormat: + 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + path: '/saml/callback', + logoutUrl: 'http://foo.slo', + signatureAlgorithm: 'sha256' + }; + + function getSamlOptions (req, fn) { + fn(null, samlOptions); + strategy._saml.options.should.containEql(samlOptions); + done(); + } + + var strategy = new MultiSamlStrategy( + { getSamlOptions: getSamlOptions }, + verify + ); + strategy.logout(); + }); +});