I have a express app with express-session, session-file-store and passport. When a user first logs in, server creates a sessionID and creates the corresponding session file. However, I have the following problems:
When the User-Agent or, curl sends a cookie with the connect.sid, I can't lookup the session store for the saved ID. I looked into session-file-store docs and could not find any methods or ways of doing so. Is there a way?
Is there a way to destroy or remove the session at 'logout` request?
Edit
As #jfriend00 suggested, I thought it best to provide more context. As mentioned earlier, I have around 3 middleware for session. This is how they are mounted:
authserver.js
// session options
const sessOpts = {
genid: (req) => uuid(),
store: new sessionFS(),
secret: _secret,
resave: false,
saveUninitialized: true
};
// mws
authservice.use(initLogger);
authservice.use(exprSession(sessOpts));
authservice.use(bodyParser.json());
authservice.use(bodyParser.urlencoded({ extended: false }));
authservice.use(expSanitizer()) // must follow preceeding line
authservice.use(corsHandler);
authservice.disable('x-powered-by');
authservice.disable('etag');
// session mws
authservice.use(passportInst.initialize());
authservice.use(passportInst.session());
This how the passport LocalStrategy is set:
passport_strategies.js
passport.use(new passportLocal(
(username, password, done) => {
//console.log('In local');
mongoHelpers.connect((cerr, db) => {
if(!!cerr) return done(cerr);
mongoHelpers.findUser({
username: username,
password: password
}, db, (err, usrs) => {
if(!!err) return done(err);
if(!!!usrs) return done(null, false);
//if(!validateUser(usrs[0], userSchema)) return done(null, false);
//console.log('local: ' + usrs.toString());
//console.log(usrs);
return done(null, usrs[0]);
});
});
}
));
passport.serializeUser((user, done) => {
console.log('In serializer');
if(!!!user) {
done(new Error("User cant be undefined in serializeUser"), null);
//console.log("user is null");
}
done(null, user._id); // mongo auto id
});
passport.deserializeUser((id, done) => {
console.log('In Dcserializer');
console.log(id);
});
Now there are two sessions exprSession which is an instance of express-session and passport.session(). Are both these instances referring to the same object?
I am looking up the user collection on each authentication strategy for matching pair or username and password. This lookup is also done in another middleware down the chain. Is the second middleware redundant?|
In the serialize user method, I am passing the mongo id. In deserialize, I get a session ID back as id. If this is the session ID, is there a way to use this to destroy session?
When a user logs out, is it a good practice (or even done generally) to remove/unset the previously set cookie even with maxAge or expires set?
I apologize if the question seems somewhat convoluted. I am fairly new to these middleware.
Related
I am struggling with getting Google OAuth to work with my Express/React application whilst using Passport.js. I am using JWTs, not sessions.
In my React webapp client, I have a "login with Google" button that calls my backend API /auth/google/ with the following route setup in Express:
router.get('auth/google', passport.authenticate('google', {session: false, scope: ['email','profile']}) );
My Passport.js google strategy is:
const googleStrategy = new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:3000/api/v1/auth/google/callback",
passReqToCallback : true
},
async (request, accessToken, refreshToken, profile, done) => {
try {
console.log('profile', profile);// ** CORRECT USER PRINTED **
let existingUser = await User.findOne({ 'google.id': profile.id });
// if user exists return the user
if (existingUser) {
console.log('Found existing user...');
return done(null, existingUser);
}
// if user does not exist create a new user
const newUser = new User({
method: 'google',
googleId: profile.id,
profileImage: profile.photos[0].value,
firstName: profile.name.givenName,
lastName: profile.name.familyName,
shortName: profile.displayName,
});
await newUser.save();
return done(null, newUser);
} catch (error) {
return done(error, false)
}
}
);
My Google developer dashboard is setup to call the following URL in my Express API backend upon successful authentication: /auth/google/callback
My Express route for this is defined as: router.get('auth/google/callback', passport.authenticate('google', {session: false}), authController.googleAuthCallback);
My Express googleAuthCallback function is defined as:
exports.googleAuthCallback = async (req, res) => {
console.log(req.user) // ** WRONG USER PRINTED HERE ** different from above user printed in google strategy
}
The strange this is when I console.log the profile variable in my googleStrategy, I get the right user profile information for the account from Google. This means the authentication vis a vis Google is fine. However, this same account is NOT being provided to my /auth/google/callback endpoint in the req.user object at that location. It is an entirely different account (it is the first value from my database of Users, which is authenticated using local authentication).
How do I get the user object back to my Express callback endpoint that I supplied to Google in the developer console as the authorized redirect URI?
As a general question, what happens after the strategy calls return done(null, existingUser);? I have no callback in the /auth/google route after the passport.authenticate() middleware is called so what happens next?
I am using "passport-google-oauth20": "^2.0.0"
My let existingUser = await User.findOne({ 'google.id': profile.id });
line was incorrect and was essentially returning no user. Mongoose does not complain and hence the strategy was just returning the first user from my database rather than the authenticated google user.
So I'm making an app with profiles and stuff. And the user would connect to his profile by using the route /user/:id (the :id would be req.user.id) the thing is when I try to log in users with same username req.user is the same for both eventhough they have different email/credentials. And I think it's because I'm using passport and when serializing a user, and saving his credentials to the session is saving the username, and of course when desirializing it's going to find the user by his username. I've already tried to change the session key to be email or id, so it would not find users with same username but I can't make it work.
Here is the code
passport.serializeUser(User.serializeUser(function (user, done) {
done(null, user.email)
}));
passport.deserializeUser(User.deserializeUser(function (email, done) {
user.findById(id, function (err, user) {
done(err, user)
})
}))
OUTPUT
Session {
cookie: {
path: '/',
_expires: 2021-05-11T18:40:11.634Z,
originalMaxAge: 604800000,
httpOnly: true
},
flash: {},
passport: { user: User's name }
}
As you can see eventhough I'm trying to add the email key to the session, it seems not to work.
Can someone help me fix this issue or even prupose a new solution
I would recommend looking into User.serializeUser and User.deserializeUser are affecting things. It's unclear to me why they are being passed the passport methods.
Here is an idea of a common implementation that may simplify how you are getting data and passing it to the req object.
passport.serializeUser((user, done) => {
done(null, user.email);
});
passport.deserializeUser((email, done) => {
// Mongoose query
// Find matching user based on email
const user = await User.findOne({ email }).exec();
done(null, user);
});
I implemented passport-jwt to authenticate user on protected route and also i want to check maybe the user login before creating first admin, please help me on how to do it.
this is my passport-jwt code that i have implemented
exports.getToken = function (user) {
return jwt.sign(user, config.secretKey, { expiresIn: 3600 });
};
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = config.secretKey;
exports.jwtPassport = passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
console.log("JWT payload: ", jwt_payload);
User.findOne({ _id: jwt_payload._id, }, (err, user) => {
if (err) {
return done(err, false);
} else if (user) {
return done(null, user);
} else {
return done(null, false);
}
});
})
);
If I understand your question correctly, you have authenticated a user and (s)he's logged in. Now, before creating an admin, you want to check if the currently logged in user hasn't expired or something else. Right ?
To do that:
You need to store JWT on the client-side so that whenever you call your API, you can attach the JWT in your request's authentication header. I say Authentication header because your ExtractJWT Strategy is fromAuthHeaderAsBearerToken.
With this you can attach your token to subsequent API calls headers. You also need to implement a middleware on your server-side so that the controller can verify whether the JWT in the Authorization header is valid or invalid.
Here is a good resource to understand the pipeline. Note that in this resource, they fromUrlQueryParameter as the extract strategy, but the concept is the same.
Initial question:
I'm using passportJS in my backend, and MongoStore as the session store, with a react frontend.
What I'm trying to do is allow the user to have a persistent login session. There's already a cookie that's getting automatically set by express-session called connect.sid. However the session doesn't seem to be persistent because req.user is undefined on subsequent requests using the same cookie that was set on login. req.user keeps returning undefined which means that passport is not verifying the session cookie somehow.
According to passport's docs that shouldn't happen.
Each subsequent request will not contain credentials, but rather the
unique cookie that identifies the session. In order to support login
sessions, Passport will serialize and deserialize user instances to
and from the session.
So what I'm trying to understand is how exactly does passportJS deal with cookies sent by the client?
Can someone help me by explaining the steps that passport takes when it comes to that?
I'm also using passport-local-mongoose plugin in my mongoose model which includes built in authenticate/serialize/deserialize methods used in auth.js below.
Relevant parts of my code for reference:
app.js: (didn't include the full file so it can be clear since it's 100+ lines, if someone suggests it could be an issue of middleware order I'll include the full code)
//session and passport initialization
const sessionStore = new MongoStore({
mongooseConnection: mongoose.connection,
collection: "sessions",
});
app.use(
session({
secret: process.env.SERVER_SECRET_KEY,
resave: false,
saveUninitialized: false,
store: sessionStore,
cookie: {
path:"/",
httpOnly: true,
expires: 9999999999999999
}
})
);
app.use(passport.initialize());
app.use(passport.session());
auth.js
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const config = require("./config");
const User = require("./models/userModel");
passport.use(
new LocalStrategy({ usernameField: "userName", passwordField: "password" }, User.authenticate())
);
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
exports.userLogin = passport.authenticate("local", {
session: true,
failureRedirect: '/users/loginfailed'
});
users.js (login request which returns the new cookie if successful)
usersRouter
.route("/login")
.post(auth.userLogin, (req, res, next) => {
console.log(req.user); // returns a valid user on login
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.json({ success: true, message: "Login successful!" });
});
movies.js (separate request after login, previous cookie included in headers)
moviesRouter
.route("/top")
.get((req, res, next) => {
console.log(req.user); // undefined
//includes a mongodb query here and a corresponding server response
})
UPDATE (thanks to this answer):
I found the issue, it was as simple as removing the cookie option from the object that was passed to session().
changed to this:
app.use(
session({
secret: process.env.SERVER_SECRET_KEY,
resave: false,
saveUninitialized: false,
store: sessionStore,
})
);
But I still don't understand why Passport likes to ignore my cookie options. What if I want to use secure or disable httpOnly or anything else I may want to try with my cookies?
I would like to reproduce how plunker manages the anonymous accounts.
Plunker can recognise an anonymous user. For example, we can save a plunker as anonym and then freeze it. As a result,
only the same user (before clearing browser history) has the full access to this plunker (eg, save a modification, unfreeze).
if the same user opens it in another browser or other users open the same link, they can NOT save any modification; they have to fork it.
In my website, I use the local strategy of passport.js to manage named users. For example,
router.post('/login', function (req, res, next) {
if (!req.body.username || !req.body.password)
return res.status(400).json({ message: 'Please fill out all fields' });
passport.authenticate('local', function (err, user, info) {
if (err) return next(err);
if (user) res.json({ token: user.generateJWT() });
else return res.status(401).json(info);
})(req, res, next);
});
And I use a localStorage to store the token. For example,
auth.logIn = function (user) {
return $http.post('/login', user).success(function (token) {
$window.localStorage['account-token'] = token;
})
};
auth.logOut = function () {
$window.localStorage.removeItem('account-token');
};
Does anyone know if passport.js has any strategy or existing tools to manage the anonymous account like what plunker does? Otherwise, is there a conventional way to achieve this?
Passport allows anonymous auth. There is a passport anonymous strategy for the same:
app.get('/',
// Authenticate using HTTP Basic credentials, with session support disabled,
// and allow anonymous requests.
passport.authenticate(['basic', 'anonymous'], { session: false }),
function(req, res){
if (req.user) {
res.json({ username: req.user.username, email: req.user.email });
} else {
res.json({ anonymous: true });
}
});
This uses your basic strategy in place, you can substitute that with a local strategy if you're using local authentication. It falls back to an anonymous strategy in case nothing is supplied, as can be seen here:
passport.use(new BasicStrategy({
},
function(username, password, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
// Find the user by username. If there is no user with the given
// username, or the password is not correct, set the user to `false` to
// indicate failure. Otherwise, return the authenticated `user`.
findByUsername(username, function(err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
if (user.password != password) { return done(null, false); }
return done(null, user);
})
});
}
));
// Use the BasicStrategy within Passport.
// This is used as a fallback in requests that prefer authentication, but
// support unauthenticated clients.
passport.use(new AnonymousStrategy());
The full example may be found here:- https://github.com/jaredhanson/passport-anonymous/blob/master/examples/basic/app.js
Remember cookies with a longer expiration date is how anonymous user is identified. This goes the same way as any server side technology trying to authenticate user by username and password and then just sets a cookie for the http request.