Situation
Currently, my frontend is built using React and the backend server utilizes SAML authentication process with Azure AD, which is handled by an Express server. The nginx server serves the React application and acts as a proxy passing all requests prefixed with /api
to the backend server. This setup ensures that the backend server is only known to nginx and not directly exposed to the client.
Visual representation of the architecture
The issue I'm facing is that Passport does not seem to function correctly in this configuration. The sequence of events is as follows:
- The client initiates the authentication process by clicking on the authentication button, triggering a request to the
/api/login
route from React. - NGINX forwards this request to the backend server, where Passport takes control.
- Upon successful login through the IDP form, Passport successfully handles the callback function, extracting the user profile from the IDP and passing it to the backend server.
- Subsequently, the backend server redirects the client to the
/home
route, but the session information is not retained or forwarded to the client, resulting in the client not being authenticated.
Code
Here is the initialization of Passport in app.js
:
app.set('trust proxy', 1)
app.use(
session({
cookie: {
path: '/',
secure: true,
httpOnly: false,
sameSite: 'none',
domain: `https://${process.env.FRONTEND_DOMAIN}`
},
resave: false,
saveUninitialized: true,
secret: 'TOI_MEME_TU_LE_C'
})
)
app.use(passport.initialize())
app.use(passport.session())
Below is the callback route in Express:
router.post('/saml/acs', bodyParser.urlencoded({ extended: false }), (req, res, next) => {
passport.authenticate('saml', (err, user) => {
if (err) {
let error = 'generic'
if (err.message.includes('RequestDenied')) {
error = 'request_denied'
} else if (err.message.includes('unspecified')) {
error = 'unspecified'
}
return res.redirect(`https://${process.env.FRONTEND_DOMAIN}/login?error=${error}`)
}
console.log(`${user.firstname} ${user.lastname} (${user.employeeId ?? '-> no employeeId <-'}) is connected.`)
req.login(user, error => {
if (error) {
console.log('Error during login.', error)
return next(error)
}
console.log('Login successful, is authenticated?: ' + req.isAuthenticated())
return res.redirect(`https://${process.env.FRONTEND_DOMAIN}`)
})
})(req, res, next)
})
It's worth noting that both logs are displayed correctly. The second log confirms that req.isAuthenticated()
returns true.
The issue arises when the client, despite successfully connecting, is not authenticated for the backend (specifically in the /me
route):
router.get('/me', (req, res) => {
// The code does not progress beyond this point, as both the method and the user object are absent in the request
if (!req.isAuthenticated()) {
return res.status(401).send({ error: "Not connected." })
}
if (!req.user) {
return res.status(500).send({ error: "No user on the request." })
}
res.status(200).json(req.user)
})
I have attempted removing my custom error handling in the Passport callback and adjusting cookie settings, but these changes have had no effect.
Why is Passport failing to detect authentication correctly?