Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dynamic SAML configuration lookup #276

Merged
merged 1 commit into from
Sep 11, 2018

Conversation

stavros-wb
Copy link
Contributor

No description provided.

@Destreyf
Copy link

@stavros-wb I don't have any control here, but i noticed your PR and this is something i was looking into doing myself, it'd be nice to see an example how this can be used included in the PR, maybe it'll get noticed shortly and merged in.

@stavros-wb
Copy link
Contributor Author

sure @Destreyf, say you have a model IdP which has a field options containing exactly the same key, values you would configure statically. Then you could:

function fetchSamlOptions(req, cb) {
  var idpId = paramsFrom(req);
  IdP.find({ id: idpId }, (err, data) => {
    if (err) {
      return cb(err);
    }

    var idp = data[0];
    cb(null, idp.options);
  });
}

var strategy = new MultiSamlStrategy(
  {
    passReqToCallback: true,
    failureRedirect: '/',
    failureFlash: true,
    fetchSamlOptions
  },
  verify
);

@sp90
Copy link

sp90 commented Jul 27, 2018

@bergie Should we get this merged?

@sp90
Copy link

sp90 commented Jul 27, 2018

@stavros-wb i'm trying to use your version to test it out.

Whats the verify function for, and then I get this error SAML assertion expired

let verify = () => {
	console.log("test");
	return;
}

let fetchSamlOptions = (req, cb) => {
	process.nextTick(() => {
		var opts = {
			callbackUrl: 'https://great-rattlesnake-45.localtunnel.me/auth/saml/callback',
			entryPoint: 'https://introdus-dev.onelogin.com/trust/saml2/http-post/sso/817114',
			issuer: 'https://app.onelogin.com/saml/metadata/69cf9e71-c0ff-4049-bf7f-5c145b8ccbaa'
		};

		console.log("opts: ", opts);

		cb(null, opts);
	})
}

passport.use('saml', new MultiSamlStrategy({
		passReqToCallback: true,
		failureRedirect: '/',
		failureFlash: true,
		fetchSamlOptions: fetchSamlOptions
	},
	verify
), (profile, done) => {
	console.log("profile: ", profile);
	return done(null, profile);
});

@stavros-wb
Copy link
Contributor Author

It's the callback to get the user profile, which you already pass as an anonymous function.

@sp90
Copy link

sp90 commented Jul 27, 2018

@stavros-wb have you tried making all this for SPA, using rest

@stavros-wb
Copy link
Contributor Author

@sp90 I'm not sure I understand, can you please elaborate?

@sp90 sp90 mentioned this pull request Aug 6, 2018
@sp90
Copy link

sp90 commented Aug 6, 2018

@stavros-wb i made it work, everything is fine i had some trouble understanding what happend 👍

@markstos
Copy link
Contributor

markstos commented Aug 6, 2018

To have this PR considered further, add documentation for it.

@stavros-wb
Copy link
Contributor Author

@markstos I've added a configuration example and removed the What if I have multiple SAML providers... question from the FAQ

@markstos
Copy link
Contributor

@stavros-wb Thanks for this feature.

Did you consider the alternative of creating multiple instances of passport-saml instead?

To create multiple instances of passport-saml yourself, you'll end use calling
passport.use() multiple times with a unique name for each strategy. You
either need to pass the custom strategy name as the first argument to
passport.use, or use the name option described in the docs.

Each instance needs its own callbackUrl as well it's own set of routes
to handle the outbound redirection, the inbound callback URL and displaying
the metadata.

In this solution, it looks like the inbound callback URL is shared among multiple SAML IdPs, while the the outbound redirection is dynamically generated based on the lookup. Do all IdPs get the same metadata siince the inbound callback URL is the same?

Both approaches have merit, I just want to understand the differences more.

If it's merged, I'd like the fetch prefix to be renamed to get.

There are several precedents in the code of using the get prefix: getCallbackUrl, getProtocol, getLogoutResponseUrl, getAuthorizeUrl'. There are are currently no other places in the project where we use fetch instead, so the change will improve consistency.

Thanks!

@sp90
Copy link

sp90 commented Aug 21, 2018

@markstos the reason why creating multiple instances is not durable is, let's say you have a SaaS where you have numerous organisations as customers, and their employees need to login via SAML. Then you have to restart the server and code the integration for each of them instead of just getting the SAML config from the database that matches the organisation?

So the things your noting is there something I can do to help resolve these issues, and if can you point me to where I have to modify to get this up and running? @stavros-wb @markstos 👍

@sp90
Copy link

sp90 commented Aug 22, 2018

I'm testing a configuration suggestion for dynamicly loading configs and this seems to work - please let me know if you see any major issues with this (external) solution of dynamicly loading configs 👍

var SamlStrategy = require('passport-saml').Strategy;
var Passport = require('passport').Passport;

let multiConfPassport = (req) => {
  let instance = new Passport();

  instance.use('saml', new SamlStrategy(req.passportSettings, (req, profile, done) => done(null, profile)));
  instance.serializeUser((user, done) => done(null, user));
  instance.deserializeUser((user, done) => done(null, user));

  return instance.authenticate('saml', {
    session: false
  });
}

let getConfg = (req, res, next) => {
  db.getConfig()
    .then(settings => {
      req.passportSettings = settings

      next();
    })
},

route.post('/saml/callback',
  getConfg,
  (req, res, next) => {
    multiConfPassport(req)(req, res, next);
  },
  (req, res) => {
    // Do stuff with the user match with your system
  })

route.get('/saml',
  getConfg,
  (req, res, next) => {
    multiConfPassport(req)(req, res, next);
  })

@stavros-wb stavros-wb closed this Aug 27, 2018
@stavros-wb stavros-wb reopened this Aug 27, 2018
@stavros-wb
Copy link
Contributor Author

@markstos

In this solution, it looks like the inbound callback URL is shared among multiple SAML IdPs, while the the outbound redirection is dynamically generated based on the lookup. Do all IdPs get the same metadata since the inbound callback URL is the same?

yes the endpoint is the same but you can depend on the request (a body parameter, query param or even a subdomain, common in SAAS products) to select the correct configuration.

To create multiple instances of passport-saml yourself, you'll end use calling
passport.use() multiple times with a unique name for each strategy. You
either need to pass the custom strategy name as the first argument to
passport.use, or use the name option described in the docs.

I agree with @sp90 that this would take some effort to get it right. having a persistence storage (db, distributed cache) can go long ways for when you have multiple node instances and need to make live changes

@sp90

I'm testing a configuration suggestion for dynamicly loading configs and this seems to work - please let me know if you see any major issues with this (external) solution of dynamicly loading configs

that works alright and the only thing I see missing is the logout functionality (you should also dynamically lookup the specific SAML config). If you add the code for the logout functionality your code will look very similar to this PR,

I think this is useful and this library can support it out of the box. @markstos if you wish to proceed I can go on and make the styling changes

@markstos
Copy link
Contributor

@stavros-wb, I've reviewed your code again and compared it with the alternative design by @sp90.

While I appreciate keeping this library light, I think your code to add built-in support is easier for module users to use and understand, and will be simpler for us to document. Also, there is not lot of code complexity added, and it's all isolated in a new SAML strategy file. If users don't want to use this code path, they don't have to.

Bottom line: please proceed with the requested refinements. I intend to merge this. Thanks!

@stavros-wb stavros-wb force-pushed the multi_saml branch 2 times, most recently from 360813b to 212bd68 Compare August 28, 2018 07:48
@stavros-wb
Copy link
Contributor Author

@markstos applied change and rebased to master

@sp90
Copy link

sp90 commented Aug 28, 2018

@stavros-wb & @markstos should the dynamic config be a wrapper module to the main lib so not everyone needs the full lib, but only need to get the dynamic config loading wrapper if they need it?

@markstos
Copy link
Contributor

@sp90, @stavros-wb, it's true this functionality doesn't need to be in the main module.

As I look now, I see that the "Multi-SAML Strategy" only uses the publicly exported "strategy" and "saml.SAML" objects, so it could be also be distributed as passport-multi-saml or passport-dynamic-saml.

While it's appealing to maintain less code and docs in the core, I think it will be more convenient for users to have this here. Here's a small suggested refinement- don't export the function from index.js, so it's not loaded if it's not needed.

From the Node.js docs:

It is possible to require specific files or sub modules distributed with a module by including a path suffix after the module name. For instance require('example-module/path/to/file') would resolve path/to/file relative to where example-module is located.

So something like this could be made to work:

const MultiSamlStrategy = require( 'passport-saml/MultiSamlStrategy' );

That would provide the convenience of a single distribution, but would not require loading the code if it's not needed. Sound good @sp90 and @stavros-wb ?

@stavros-wb
Copy link
Contributor Author

@markstos i've removed the export from the index

README.md Outdated
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. these docs need to be updated to match as well. Hopefully we can make:

const MultiSamlStrategy = require( 'passport-saml/MultiSamlStrategy' );

And not this uglier and more confusing syntax:

const MultiSamlStrategy = require( 'passport-saml/lib/passport-saml/MultiSamlStrategy' );

This might involve moving the MultiSamlStrategy.js file, but I haven't tested.

@stavros-wb
Copy link
Contributor Author

There 3 ways to do this AFAIK

  1. export the new strategy from index. and users can use it with
const { MultiSamlStrategy } = require('passport-saml')
  1. leave the strategy in lib/passport-saml
lib/
└── passport-saml
    ├── index.js
    ├── inmemory-cache-provider.js
    ├── multiSamlStrategy.js
    ├── saml.js
    └── strategy.js

and users can use it with

const MultiSamlStrategy = require('passport-saml/lib/passport-saml/multiSamlStrategy')
  1. move strategy to parent directory
LICENSE
README.md
docs
└── adfs
    ├── NameIDFormatError.jpg
    ├── README.md
    └── retrieve_adfs_certificate.sh
lib
└── passport-saml
    ├── index.js
    ├── inmemory-cache-provider.js
    ├── saml.js
    └── strategy.js
multiSamlStrategy.js

and users can use it with

const MultiSamlStrategy = require('passport-saml/multiSamlStrategy')

I would prefer the first options, which I think is cleaner. I don't think adding a few more functions will be of any consequence to users that don't wish to use the feature.

@markstos how'd you like to proceed?

@markstos
Copy link
Contributor

markstos commented Sep 7, 2018 via email

@stavros-wb
Copy link
Contributor Author

@markstos done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants