I am trying to use passport-saml to authenticate users with a SAML IdP. As Frontend I am using React JS with Vite and as Backend I am using express.js with express-session as session manager.
My problem is that I use a separate authentication system for my users in addition to Passport. This authentication system already uses express-session to store various data about the user sessions, including whether the user is currently authenticated and which user it is. My goal now is to connect this authentication system to that of passport-saml so that my users can log in through my authentication system and add another authentication method to access their account through passport-saml. Unfortunately, this is what happens:
my passport Strategy Config looks like this:
passport: {
strategy: 'samlStrategy',
saml: {
callbackUrl: process.env.BACKEND_BASE_URL + "/api/eid-saml/callback",
entryPoint: // the saml idp i am using,
issuer: // my web app's name,
cert: // idp cert,
privateKey: // my private key,
passReqToCallback: true
}
}
My Strategy itself is pretty simple:
const samlStrategy = new Strategy(
config.passport.saml,
function (req, profile, done) {
try {
return done(null, profile);
} catch(error) {
return done(null, false, "ErrorMessage:" + error);
}
}
)
And finally here are my login and callback endpoints:
router.get("/login", function(req, res, next) {
console.log(req.session);
passport.authenticate("samlStrategy", {
successRedirect: '/api/eid-saml',
failureRedirect: '/api/eid-saml/login',
session: false
})(req, res, next);
}
);
router.post("/callback",
function(req, res, next) {
console.log(req.session);
res.status(200).send("Authentifiziert!");
}
);
From the frontend I am redirecting my users to my login endpoint using window.location.href
to start the SAML login procedure. If I now compare the request session that I get on my login endpoint:
Session {
cookie: {
path: '/',
_expires: 2023-06-03T12:47:32.166Z,
originalMaxAge: 3600000,
httpOnly: true,
secure: false,
domain: null,
sameSite: true
},
isAuthenticated: true,
userName: //a user name,
userId: // a user id,
}
with the one that I get when the callback endpoint is called:
Session {
cookie: {
path: '/',
_expires: 2023-06-03T12:48:09.301Z,
originalMaxAge: 3600000,
httpOnly: true,
secure: false,
sameSite: true
}
}
I can see that the properties coming from my own authentication system are missing now. That way, I am not able to link the data coming from the SAML response with the data coming from my own authentication system. I am not able to understand why the session is working on my login endpoint but not on my callback endpoint since both endpoints are available over the same domain. Is there any way to resolve this issue or am I missing something that prevents me from reading the session data in my callback endpoint?
I was actually able to resolve the problem in the meantime. I think the problem occurred because the Identity Provider executed a redirect to the Callback endpoint of my Service Provider implementation directly on the server side. Since the cookie that contained the session ID was only available in the cookie storage of the user agent, the request to my callback endpoint did not contain the cookie information anymore. My solution was:
I abandoned passport-saml and passport-js in favor of saml2-js because I found it helped me to easier achieve my goal and npm was showing security warnings for passport-saml, also saml2-js has a pretty good code example in its documentation
When the callback endpoint is called: first extract the SAML Response from the request object, then send HTML code containing a form to the user agent which gets auto-submitted using javascript - this is also described here.
Instead of using jQuery as described there I used the document.getElementById() method:
const samlCallback = (req, res) => {
let options = {
request_body: req.body,
require_session_index: false
};
return res.status(200).send(
"<html><body><form style=\"display: none\" action=\"/api/eid-saml/assert\" method=\"POST\" id=\"samlForm\">" +
"<input type=\"hidden\" id=\"samlOptions\" name=\"samlOptions\" value='" + escape(JSON.stringify(options)) + "'/>" +
"</form>" +
"<script>document.getElementById(\"samlForm\").submit();</script>" +
"</body></html>"
);
}
this way I got the user agent that has the cookie information to automatically post the extracted SAML Response together with the cookie information to another endpoint (/api/eid-saml/assert) where I now am able to extract information from the cookie session and also from the SAML response:
const assertSaml = (req, res) => {
let samlOptions = JSON.parse(unescape(req.body?.samlOptions))
sp.post_assert(idp, samlOptions, (err, saml_response) => {
if (err != null) {
return res.status(500).send("ServerError: " + err);
} else {
// do something with req.session and saml_response
return res.status(200).send("Already existing user session and SAML session information are now linked together, nice.");
}
}
);
}