how do I redirect back to the originally-requested url after authentication with passport-saml?

15,476

Solution 1

Can I send a url, or some other value, to the IdP that will get round-tripped and POSTed back in the response SAML? And if so, how can I access it in my /login/callback route?

To round-trip a value via the IdP you need to use RelayState. This is a value that you can send to the IdP and, if you do, they are obliged to send it back without alterations.

Here is the what the SAML specifications has to say:

3.1.1 Use of RelayState

Some bindings define a "RelayState" mechanism for preserving and conveying state information. When such a mechanism is used in conveying a request message as the initial step of a SAML protocol, it places requirements on the selection and use of the binding subsequently used to convey the response. Namely, if a SAML request message is accompanied by RelayState data, then the SAML responder MUST return its SAML protocol response using a binding that also supports a RelayState mechanism, and it MUST place the exact RelayState data it received with the request into the corresponding RelayState parameter in the response.

To use this with passport-saml you must add it as an additionalParams value. The code below shows this happening.

saml = new SamlStrategy
    path: appConfig.passport.saml.path
    decryptionPvk: fs.readFileSync(appConfig.passport.saml.privateKeyFile)
    issuer: appConfig.passport.saml.issuer
    identifierFormat: tenant.strategy.identifierFormat
    entryPoint: tenant.strategy.entryPoint
    additionalParams:{'RelayState':tenant.key}
    ,
    (profile, next) -> 
        # get the user from the profile

The code above is from a multi-tenant saml implementation so I am sending my tenant.key as the RelayState param. I then retrieve this value from the body of POSTed return from the IdP and use it to re-establish all the state I need.

getTenantKey: (req, next) ->
    key = req.body?.RelayState ? routes.match(req.path).params.tenentKey
    next null, key

Your case might be simpler. You will probably want to store the final-destination url in a time limited cache and then send the cache-key as the RelayState param.

For what it is worth, you can avoid using RelayState altogether if you just use the original SAML request-id as your cache key. This value is always sent back to you via the InResponseTo field.

Solution 2

If you want to specify the RelayState on a per-request basis, you would think you could do:

passport.authenticate('saml', { additionalParams: { RelayState: "foo" } })

But that does not work. If you look at the implementation: https://github.com/bergie/passport-saml/blob/6d1215bf96e9e352c25e92d282cba513ed8e876c/lib/passport-saml/saml.js#L326 you will see that the additionParams are picked up from the initial configuration options OR from the req object (but not from the per-request options).

But luckily the Passport SAML strategy does this per-request:

var RelayState = req.query && req.query.RelayState || req.body && req.body.RelayState;

Then it looks in the config options for more (global) additionalParams.

So if you want to specify RelayParams on a per-request basis, you have to load them into req before authorize is called. In my case I do something like this inside of the route handler:

req.query.RelayState = req.params.redirect_to;
passport.authenticate('saml')(req, res, next);

Then when the redirected request comes back from the SAML IdP, you should be able to use req.body.RelayParams to access your per-request state.

Note that if your RelayParams value is an object, you may have to use JSON.stringify to encode it into RelayParams, and JSON.parse to decode it back out (I had to do that in my case).

Share:
15,476
Dave Stearns
Author by

Dave Stearns

I teach at the University of Washington's Information School

Updated on June 14, 2022

Comments

  • Dave Stearns
    Dave Stearns about 2 years

    Sorry if this is a bonehead question, but I'm having some trouble understanding how I might redirect the client browser back to whatever URL was originally requested after a successful authentication with our SAML identity provider (IdP). I'm using the latest versions of passport-saml, passport, and express.

    For example, say the client originally requested /foo/bar from a link on another unprotected page, but since that is a protected resource, I respond with a redirect to /login, which is where I call passport.authenticate('saml').

    app.get('/login', passport.authenticate('saml'));
    
    function ensureAuth(req, res, next) {
        if (req.user.isAuthenticated()) {return next();}
        else {res.redirect('/login');}
    }
    
    app.get('/foo/bar', ensureAuth, function(req, res) {
        ...
    });
    

    That call will redirect the browser to my IdP's sign-on page, and after a successful authentication, the IdP POSTs back to my /login/callback route. In that route, I again use passport.authenticate(saml) to validate the response SAML, and if all is good, I then get to redirect the browser back to the requested resource...but how do I know what that requested resource was? Because it's a POST callback, I've lost any state associated with the original request.

    app.post('/login/callback', passport.authenticate('saml'), function(req, res) {
        res.redirect('...can I know which url to redirect back to?...');
    });
    

    The example in the passport-saml readme just shows a hard-coded redirect back to the root resource, but I would want to redirect back to the originally-requested URL (/foo/bar).

    Can I send a url, or some other value, to the IdP that will get round-tripped and POSTed back in the response SAML? And if so, how can I access it in my /login/callback route?

    Or is there some better, express/passport way to do this that I'm missing?

    Any help you can provide would be most appreciated!

    • r3wt
      r3wt about 10 years
      store the original request url in a session cache expiring in 10 minutes. then retrieve it and destroy it on success redirecting the user in the process
    • Dave Stearns
      Dave Stearns about 10 years
      Ah, yes, that would work! Thanks for the idea. It would be much cleaner if I could send some state to the IdP and get it back again, however. I looked at the SAML 2.0 spec link and it looks like you can specify a different callback with each authentication request via the AssertionConsumerServiceURL element, but it does note that the "The responder MUST ensure by some means that the value specified is in fact associated with the requester" (49). Perhaps specifying different values each time doesn't really work in practice.
    • r3wt
      r3wt about 10 years
      well, you need to verify the referrer url always. otherwise, you will have hackers abusing your scheme to redirect to malware. ie, they get an email posing as your domain, with a link to some site that redirects to your login page. when they login, they are redirected back to the referrer, stealing your users cookies.
    • Dave Stearns
      Dave Stearns about 10 years
      Yes, I was thinking that one might use different callback urls within the same domain. I would expect the IdP to reject any auth request where the callback URL was from a domain other than the one registered with the IdP.
  • ahoffer
    ahoffer over 8 years
    How did you do the multi-tenant saml? Something off of NPM or did you roll it yourself? I will eventually need to support multiple identity providers.
  • Chris
    Chris about 8 years
    Is it possible to set the RelayState after the strategy has been created? Early in the boot of my server I have "passport.use(new SamlStrategy(" like you have above. During runtime I have "passport.authenticate("... Is it possible to provide the RelayState then? I would know better during runtime what should be returned.
  • That1guyoverthr
    That1guyoverthr almost 8 years
    @Chris did you even find an answer your question, "Is it possible to set the RelayState after the strategy has been created"? I'm wondering the same thing.
  • Chris
    Chris over 7 years
    @That1guyoverthr I never found a good solution. I was reconstructing passport on each call to inject the new relay state for that user. However that relay state was a singleton and if users hit it at the same time, one would get the relay state of the other user! I gave up and am using session to capture where the user is before they attempt to log in.
  • MForMarlon
    MForMarlon over 7 years
    @Chris Just to clarify, are you still using RelayState, or just doing a straight redirection after authentication? I'm having trouble figuring out how to elegantly solve this without using sessions or cookies.
  • Chris
    Chris over 7 years
    @MForMarlon After some battles I gave up on the relay state and just use session. Basically I save the user's location in a session when they attempt to log in, then access that same session when they com back. I don't like it but it's what I saw Java doing.
  • radicand
    radicand over 6 years
    Thanks, this is exactly what I needed to make this work.
  • Farhan Tahir
    Farhan Tahir about 6 years
    Thanks for this solution. I was looking for something like this.
  • Andrew
    Andrew about 5 years
    This worked perfect for me. The accepted answer puts it in the Strategy, which isn't dynamic per-request. This solution puts the parameter in req.query.RelayState, which you can then pick up in the callback Post from req.body.RelayParams. Perfect!
  • Andrew
    Andrew about 5 years
    For those of you unable to make this solution work, look at the @BobDickinson solution
  • Aaron Gong
    Aaron Gong almost 5 years
    This works abd should be the answer for per request handling. The property RelayState is so very important