WebApp.connectHandlers - SSO / SAML in Meteor


#1

Hey Meteor folks! I could really use your help! :sweat:

Since going live with my SaaS Meteor app about a week ago, it’s been a bumpy start. Our first customer authenticates their users over SAML. There’s a couple Meteor SAML implementations out there on atmosphere, so we’ve basically adapted one of those for internal use. We’re also using meteor-cluster.

Something like 1 in 5 of my customer’s end users cannot login to my Meteor app. When the issue it happens it’s totally random. That’s a terrible quality of service and I’m ashamed of it. I didn’t catch this issue in pre-prod testing, either, and I have been unable to reproduce the bug in any other environment, and I’m quite frankly feeling desperate and falling to a pit of despair. I don’t think I’ve faced a more challenging bug in my coding career.

But anywho, do we have any experts using the WebApp.connectHandlers api in here?

Am I supposed to wrap the entire body like so in a Fiber? Does anything about this code look fishy?

var Fiber = Npm.require('fibers'),
    bodyParser = Npm.require('body-parser');

WebApp.connectHandlers
  .use(bodyParser.urlencoded({ extended: false }))
  .use('/SSO/SAML2', function (req, res, next) {
    Fiber(function() {
      var strategy = AuthStrategies[req.headers.host];
      if (req.method === 'GET') {
        strategy._saml.getAuthorizeUrl(req, Meteor.bindEnvironment(function (err, result) {
          res.writeHead(302, {'Location': result});
          res.end();
        });
      } else if (req.method === 'POST') {
         strategy._saml.validatePostResponse(req.body, Meteor.bindEnvironment(function (err, result) {
           // Meteor.users.upsert({...}, {...});
           // Send me a debug email (e.g., Mailer.send({to: '', ...});
           //
           // Occasionally, I'll recieve the email but no user object
           // was ever created despite a validated Post Response (err = null)
          ...
        }));
      } else {
        return next();
      }
    }).run();
  });

Any feedback, spitballing, etc is greatly appreciated, thanks!

Since the issue where people have difficulty logging in only occurs when traffic picks up, I am thinking it’s a concurrency issue, but I’m not completely sure.


#2

Heed my warning, some of these published Meteor packages are no good-- due your due diligence! They might work in isolated development environments, but these other implementations traverse an inefficient amount of tightly-coupled code making it difficult to diagnose in corner case, irregularly-occurring bugs.

I crafted my own SAML integration implementation, with a few key differences:

  • Rather than using Accounts.loginHandlers and passing a randomly generated client string around via a popup flow, do a hard redirect to the SAML Identity Provider’s entry point.
  • Once validating the SAML response:
    • generate a stamped login token,
    • store the hashed token in the database (on the user document via Accounts.insertLoginToken), and
    • send the token in the response as a cookie.
  • On the client, login when JavaScript kicks in if the cookie is present.

Here’s some code snippets from the key points above:

Server code:

var Fiber = Npm.require('fibers'),
    bodyParser = Npm.require('body-parser'),
    cookieParser = Npm.require('connect-cookies'),
    saml = Npm.require('passport-saml');

WebApp.connectHandlers
  .use(bodyParser.urlencoded({ extended: false }))
  .use(cookieParser())
  .use('/SSO/SAML2', function (req, res, next) {
    Fiber(function() {
      if (req.method === ‘GET’) {
        ...
      } else if (req.method === 'POST') {
        ...
        var user = Meteor.users.findOne({...});
        var stampedLoginToken = Accounts._generateStampedLoginToken();
        Accounts._insertLoginToken(user._id, stampedLoginToken);
        res.cookies.set('token', stampedLoginToken.token, { httpOnly: false });
      }
    }).run();
  });

Client code:

Meteor.startup(function () {
  function getCookieValue(a, b) {
    b = document.cookie.match('(^|;)\\s*' + a + '\\s*=\\s*([^;]+)');
    return b ? b.pop() : '';
  }
  var token = getCookieValue('token');
  if (token) {
    Meteor.loginWithToken(token);
  };
});

I’m glad I was able to get through that very tricky bug.


#3

Thank you so much for this, it has been extremely to useful to me!


#4

Glad I was able to help. I was wondering when I posted my reply if anyone would find value in it lol.


#5

Thanks @peterm complete Newby to SAML… I am trying to get this working… but need an idiots guide pls and could not workout what the ... should be?

Am I meant to be using XPATH and getting the nameID/email address out of the req param?

Any help appreciated

BTW, I was looking at using this node package as an alternative https://www.npmjs.com/package/saml2-js#IdentityProvider , any thoughts?


#6

In the abridged GET code, I was making an internal call to generate the redirect URL from my app (the “Service Provider” or SP as it’s sometime called) to the Identity Provider (IdP).

In the abridged POST code, which is called by the IdP, I validate the response and parse some information out of the response (e.g., e-mail address, name, etc) to update the user object for the user logging in. Usually I’ll key off of some sort of persistent data identifier in the POST response (e.g, some piece of data that can act as a primary key), to ensure I’m always looking up the correct user.


#7

Thanks @peterm about to give it a go… will try with the new saml2 package above


#8

Thanks again for the info.

So I can get the identity of a user and decide to either create a new user or just add the login using Accounts._insertLoginToken

after that I tried to redirect to the app with:

                res.cookies.set('token', stampedLoginToken.token, { httpOnly: false });
                res.writeHead(301, {'Location': Meteor.absoluteUrl()});
                res.end()

which should then run the startUp code… but does not.

oops hit save too soon, more info…

Will that run Meteor.startup and the code to get the cookie?

Meteor.startup(function () {
    function getCookieValue(a, b) {
        b = document.cookie.match('(^|;)\\s*' + a + '\\s*=\\s*([^;]+)');
        return b ? b.pop() : '';
    }
    var token = getCookieValue('token');
    console.log("SAML CLIENT:",token)
    if (token) {
        Meteor.loginWithToken(token);
    };
});

ie my redirect does not seem to be working from within the Fiber.

Any ideas pls?


#9

@adamginsburg The second code snippet is client side code. Are you running it on the client?


#10

Hi, yes the "getCookie… " stuff? If so it’s on the client.

I actually ran a nonSAML test with it and it works after a redirect from the server…it sets the cookie and show’s up.
Ie

  .use('/test', function (req, res, next) {
        console.log("REQ:", req);
        console.log("RES:", res);
        res.cookies.set('token', "SOMETESTCOOKIE", { httpOnly: false });
        res.writeHead(301, {'Location': Meteor.absoluteUrl('sign-in')});
        res.end()

    })

The cookie gets set.

However, when I run from inside the assert callback, for some reason it does not seem to be working. This is a test where it’s IE calling the login via the IDP. I can see all’s good on the server… just the redirect code above does not seem to be working.

It’s been harder than usual to debug as I don’t actually have a login to the test system, so relying on others.

Any thoughts?

BTW, will this work with Mobile/Cordova? Thinking that this will only work if I spawn the login from within the Cordova app, otherwise it won’t have the cookie.


#11

Try setting up a dummy instance to run against testshib.org. This site can act as a fake SAML identity provider. Their login screens have username and password combinations for you to use to attempt logging in. Then you can add breakpoints or console.log statements in your own code.


#12

Thanks.

I actually got it working. I had to have another Fibre within a Fibre inside the callback.


#13

@peterm thanks again for the help here. I basically have it working.

One thing I was wondering how yo dealt with it on Cordova with open up the login in a popup etc…

I have seen a few examples for Oauth and the other SAML packages… just wondering if and how you conquered this?

thanks


#14

Hi @peterm just wondering if you had any comments on how the login flow should work on Cordova, pls?


#15

@adamginsburg I haven’t used Cordova-- so I would have no idea. Auth0 tends to provide good discussion on all things auth. It looks like they’re recommending the InAppBrowser Cordova plugin.