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

Dave Stearns picture Dave Stearns · Jul 7, 2014 · Viewed 11.7k times · Source

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!

Answer

biofractal picture biofractal · Dec 14, 2014

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.