Reputation: 1179
My Express app calls a request.isAuthenticated() method. However, I don't know what it checks to determine whether it's authenticated. My app needs to authenticate via OIDC. How do I tell the isAuthenticated() method that it passed OIDC authentication?
Currently, I have it set to redirect to the OIDC authorize endpoint with an appropriate client_id, scope. The user's browser follows the redirect, the user successfully logs in. OIDC sends a redirect back to the callback provided by my Express app. The users browser reaches this endpoint successfully.
My consolidated file in one posting below. Because I'm new to Node, it's sloppier than I'd like. Also, because I can't get Visual Code to catch my breakpoints (see my other related post), I can only debug using console.log statements.
If I go to /cost-recovery in the browser, it goes to this route:
app.use('/cost-recovery*', saveUrlInSession, /*ensureAuthenticated*/ isLoggedIn,createProxyMiddleware(sprint_cost_recovery_options));
It saves the URL in the session, allowing the callback to go where I want. That works. In both the ensureAuthenticated and isLoggerdIn handlers, the system redirects to the OIDC/OpenId/?? ID login page. I'm able to log in, and it goes back to my callback page. In that callback route, the req.isAuthenticated() still says false.
Perhaps because I'm using this passport module, it has no idea that the login happened. There are cookies being set, in a pinch I can just check those in lieu of a working req.isAuthenticated() method, but I'd rather use the tools provided.
/**
* How the application respond to clients requests depending of the endpoint
*/
const userController = require('../controllers/userController');
var OpenIDConnectStrategy = require('passport-ci-oidc').IDaaSOIDCStrategy;
const strategyConfiguration = require('../../config/strategy.json');
console.log('strategyConfiguration=' + JSON.stringify(strategyConfiguration));
const passport = require('passport');
const https = require('https');
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
console.log('App setting title=' + app.get('title'));
console.log('App env=' + app.get('env'));
console.log('App setting query parser=' + app.get('query parser'));
console.log('App setting string routing=' + app.get('strict routing'));
console.log('App setting case sensitive routing=' + app.get('case sensitive routing'));
var http = require('http');
var url = require('url');
var currentOriginalUrl;
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (obj, done) {
done(null, obj);
});
// openid-client is an implementation of the OpenID Relying Party (RP, Client) server
// for the runtime of Node.js, support passport
//OAuth 2.0 protocol
//middleware Passport-OpenID Connect
const config = require('../configuration/config').getConfiguration();
console.log('config=' + JSON.stringify(config));
console.log('strategyConfiguration=' + JSON.stringify(strategyConfiguration));
var OpenIDConnectStrategy = require('passport-ci-oidc').IDaaSOIDCStrategy;
var Strategy = new OpenIDConnectStrategy({
discoveryURL: strategyConfiguration.discoveryURL,
clientID: strategyConfiguration.clientID,
scope: 'openid',
response_type: 'code',
clientSecret: strategyConfiguration.clientSecret,
callbackURL: strategyConfiguration.callbackURL,
skipUserProfile: true, /* this was true before */
CACertPathList: [
`/certs/DigiCertGlobalRootCA.crt`,
`/certs/DigiCertSHA2SecureServerCA.crt`,
]
},
function (iss, sub, profile, accessToken, refreshToken, params, done) {
process.nextTick(function () {
profile.accessToken = accessToken;
profile.refreshToken = refreshToken;
const userDetails = profile._json;
const userProfile = {
uid: userDetails.uid,
mail: profile.id,
cn: decodeURIComponent(userDetails.cn),
exp: 60 * 60 /*TODO: Get proper number of seconds. userDetails.exp */,
blueGroups: userDetails.blueGroups,
};
done(null, userProfile);
})
}
)
var proxy_server = require('http-proxy').createProxyServer({});
const originalUrl = new URL(config.host);
console.log('matched cost-recovery using original url: ' + originalUrl);
const newUrl = new URL(originalUrl);
newUrl.port = 8447;
console.log('matched cost-recovery new url' + newUrl);
function saveUrlInSession(request, response, next) {
if (request.params.state) {
console.log('Saving state=' + request.params.state + " in session");
request.session.savedUrl = request.request.params.state;
} else {
console.log('Saving originalUrl=' + request.originalUrl + " in session");
request.session.savedUrl = request.originalUrl;
}
if (next) {
return next();
} else {
console.log('@@ no next');
}
}
function ensureAuthenticated(req, res, next) {
if (!req.isAuthenticated()) {
console.log('@@ ensureAuthenticated reached. Not authenticated. redirecting to /login');
res.redirect('/login')
} else {
console.log('@@ ensureAuthenticated reached. Authenticated. Continuing to next handler');
return next();
}
}
function isLoggedIn(req, res, next) {
if (req.isAuthenticated()) {
console.log('@@isLoggedIn req.isAuthenticated()=true');
req.session.isAuthenticated = true;
res.locals.isAuthenticated = true;
res.locals.user = req.user;
next(); //If you are authenticated, run the next
} else {
console.log('@@isLoggedIn req.isAuthenticated()=false');
return res.redirect("/login");
}
}
function getUserProfile(req, res, next) {
console.log('@@ reached getUserProfile')
if (typeof req.user == 'undefined') {
res.status(401);
next();
}
return res.status(200).send(req.user);
}
function getUserName(req, res, next) {
console.log('@@ reached getUserName')
if (typeof req.user === 'undefined') {
res.status(401);
return next();
}
return res.status(200).send(req.user.cn);
}
var newURL = url.format({
protocol: config.protocol,
host: config.host,
pathname: config.originalUrl
});
console.log('newURL=' + newURL);
var newURL2 = new URL(newURL);
newURL2.port = "8447";
newURL2.protocol = "http";
console.log('newURL2=' + newURL2);
const sprint_cost_recovery_options = {
target: newURL2,
level: 'debug',
changeOrigin: true,
ws: true
}
console.log('@@ sprint_cost_recovery_options=' + JSON.stringify(sprint_cost_recovery_options));
passport.use(Strategy);
app.use(passport.initialize());
app.use(passport.session());
app.use(function (request, response, next) {
console.log('Common Route: Incoming request originalUrl:' + request.originalUrl);
console.log('Common Route: Incoming request previous Url:' + request.header('referer'));
console.log('Common Route: Incoming request url:' + request.url);
next();
});
app.get('/auth/sso/callback/:callback_uri?'
, function (request, response, next) {
console.log('CB-1 matched on originalUrl=' + request.originalUrl);
console.log('@@ CB-2. isAuthenticated=' + request.isAuthenticated());
console.log('@@ CB-2.5 request.account test=' + request.account);
console.log('@@ savedUrl in session=' + request.session.savedUrl);
//var redirectUrl = poppedUrlFromSession(request);
var redirectUrl = request.session.savedUrl;
if (!redirectUrl) {
redirectUrl = "/health-check";
}
console.log('@@ CB-3. redirectUrl=' + redirectUrl);
console.log('@@ CB-4. before passport.authenticate');
console.log('@@ CB-5. after passport.authenticate');
console.log('@@ CB-6. isAuthenticated=' + request.isAuthenticated());
console.log('@@ auth-sso-callback-2 bp1');
response.redirect(redirectUrl);
}
);
app.use('/login?:state?',
function (request, response, next) {
var stateIndicator = (request.params.state) ? " with state " + request.params.state : " with no state/redirect.";
console.log('@@ Reached login with ' + stateIndicator);
return next();
},
passport.authenticate('openidconnect', { state: Math.random().toString(36).substr(2, 10) }));
app.use('/rules/username', saveUrlInSession, ensureAuthenticated, userController.getUserName);
app.use('/rules/profile', saveUrlInSession, ensureAuthenticated, userController.getUserProfile);
app.use('/cost-recovery*', saveUrlInSession, /*ensureAuthenticated*/ isLoggedIn,createProxyMiddleware(sprint_cost_recovery_options));
app.use('/profile', saveUrlInSession, ensureAuthenticated, getUserProfile);
app.use('/username', saveUrlInSession, ensureAuthenticated, getUserName);
app.get('/successful-login', function (req, res) {
res.send('login succeeded');
});
app.get('/failure', function (req, res) {
res.send('login failed');
});
app.get('/health-check', (request, response) => {
response.send('Middleware is running.');
});
};
Upvotes: 6
Views: 9658
Reputation: 2346
The short answer is that most of the time req.isAuthenticated
is simply checking whether or not the value req.user
is set, but the details can change depending upon your Passport configuration.
As I think may already be clear to you, the isAuthenticated
method is added to the req
object by Passport.js.
For reasons that don't seem clear to anyone else, there doesn't seem to be any public-facing documentation for that method.
But you can find the implementation of req.isAuthenticated
(also req.isUnauthenticated
) in the source of passport's http/request.js.
The raw code is currently this:
req.isAuthenticated = function() {
var property = 'user';
if (this._passport && this._passport.instance) {
property = this._passport.instance._userProperty || 'user';
}
return (this[property]) ? true : false;
};
(Lines 77-90 in the version request.js I looked at.)
As you can see this base implementation is essentially (1) figuring out what passport's userProperty
is set to and (2) checking if req[userProperty]
is "truthy".
(The userProperty
value is another under-documented or possibly un-documented feature of Passport. You can probably just assume the value is user
unless you've taken steps to make it something else.)
So effectively isAuthenticated
should return true
if req.user
has been set to a non-null object and false
if req.user
is null
or false
or 0
, etc.
As you probably know in the general case a passport strategy will set req.user
to a map of attributes about the authenticated user as part of the req.login
function (which is indirectly invoked by the passport.authenticate
middleware). So in general after you invoke req.login
or passport.authenticate
you should expect req.user
to be populated and hence for req.isAuthenticated
to return true.
Since that's not happening my guess would be that one of these is happening.
The login
function or authenticate
middleware isn't actually being invoked when you expect it to be.
The function is being called but the authentication itself is failing (so isAuthenticated() === false
is technically correct).
The authentication is successful but the profile information is not saved as req.user
.
Inspecting the request object after a known log-in might make it obvious whether or not the user info is being stored, and if so, where.
Scanning the code you shared it looks like you are also expecting the user profile information to be found in req.user
for your application, so if that's working at all I would expect req.isAuthenticated
to work, but I'm not sure I fully grok your overall status.
I'm not familiar with password-ci-oidc
in particular (and while I see the module on npm it doesn't look like the raw source code is publicly available), but you might want to dig to check whether req.user
is the userProperty
it is using and/or whether it's populating a user-object at all.
In particular the skipUserProfile: true
bit in your strategy configuration jumps out at me. Is it possible that you're actually telling the strategy middleware not to populate req.user
?
Independent of your original question, the other thing to note is that req.isAuthenticated()
isn't doing much more than req.user ? true : false
so if you have a more reliable way to validate that the user is authenticated it maybe sufficient to just use that instead (or to monkey-patch req.isAuthenticated
to use your logic instead of the default behavior). It doesn't seem like it's doing much more than that anyway.
Upvotes: 4