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!
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.