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);
});
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.
Setup
I am doing web site authorization, and want to embed best practices into it, while keeping code clean and readible. For now I have classic code like this:
let foundUser = await userModel.findOne({ email: recievedEmail });
if(!foundUser)
error("not authorized!");
const isPasswordMatch = await bcrypt.compare(recievedPassword, foundUser.password);
if(!isPasswordMatch)
error("not authorized!");
foundUser.update({ $set: { lastLogin: new Date() }, $push: { myEvents: authEvent } });
foundUser.save();
success("authorized OK!");
Meanwhile, I've asked a question on the best mongoose command to perform auth, and we've forged up the following "auth-check-and-update" command, in an "atomic" manner:
const foundUser = await userModel.findOneAndUpdate(
{ email: recievedEmail, password: recievedPassword },
{ $set: { lastLogin: new Date() }, $push: { myEvents: authEvent } }
);
if(foundUser)
success("authorized OK!");
else
error("not authorized!");
Idea here is obvious - if a user with matching email and password is found then user is considered as authorized, and its last login timestamp is updated (simultaneously).
Problem
To combine best practices from the two above, I need somehow to embed bcrypt.compare() call inside findOneAndUpdate() call. That is tricky to do, because I cannot just "compare hashed passwords"; bcrypt just works differently from simple hashes (like sha or md5): For security reasons it returns different hashes every time. (Answers in the link explains "why and how").
Solution Attempt
I've looked into mongoose-bcrypt package: it is utilizing Schema.pre() functionality:
schema.pre('update', preUpdate);
schema.pre('findOneAndUpdate', preUpdate);
To get the idea, please, take a look at mongoose-bcrypt\index.js.
You will see, that preUpdate affects only creating new user (..andUpdate part), but not actual checking (findOne.. part). So this plugin could fit for implementing "user registration" / "change password". But it can't work for authorization in the proposed way.
Question
How would you "combine" bcrypt.compare() and userModel.findOneAndUpdate() calls under such circumstances?
What about compare password in UserModel like this
// method to compare password input to password saved in database
UserModel.methods.isValidPassword = async function(password){
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
And inside your auth or passport (i am using passport) do something like this
passport.use(new LocalStrategy(
(username, password, done) => {
// change your query here with findOneAndUpdate
User.findOne({ username: username }, (err, user) => {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.isValidPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
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.
I'm working in an application which uses a REST api using the MEAN stack and Passport JS to manage the authentication.
The authentication, we use JTW tokens for the communication between the backend and frontend. The token is generated based on local username and passwords.
Now I want to 'add' (authorize) the user's google account to the profile to use with google calendar API. (using this-> https://github.com/wanasit/google-calendar)
I've already have managed to send the user to the google authorization page, and get the token back from it. The problem is that when the user gets redirected to the page, it looses the JWT token where I check the user for the request.
Is there any other way to get the current logged in user, or to pass some custom callback authorization header/param when calling the authorize method?
auth.js:
var googleParams = {
clientID: config.auth.google.clientID,
clientSecret: config.auth.google.clientSecret,
callbackURL: config.auth.google.callbackURL
}
var googleStrategy = new GoogleStrategy(googleParams, function (token, refreshToken, profile, done) {
profile.token = token;
return done(null, profile);
});
routes:
rotas.get(
'/google',
auth.authenticate(), // will check the current user
auth.isLoggedIn, // make sure the user is really logged in
auth.authorize('google', { scope: googleScope, passReqToCallback: true }) // redirects to google to get the token
);
rotas.get('/callback/google',
auth.authorize('google', { scope: googleScope, passReqToCallback: true })
auth.authRedirect()
);
the auth.authRedirect() function above is the closest solution I've found. It's a Express middleware wich redirects the user to a known route in the frontend where the user IS authenticated... but then I would not be able to fetch all his Google profile and information i need...
You have to be sure the app.use(session) its been called before any route.
...
app.use(session({
secret: 'secret'
}))
app.use(passport.initialize())
app.use(passport.session())
...
rotas.get(
'/google',
auth.authenticate(), // will check the current user
auth.isLoggedIn, // make sure the user is really logged in
auth.authorize('google', { scope: googleScope, passReqToCallback: true }) // redirects to google to get the token
);
rotas.get('/callback/google',
auth.authorize('google', { scope: googleScope, passReqToCallback: true })
auth.authRedirect()
);
Your req.user won't be undefined in this case.
If it doen't work right way, I can put my whole code that I've created here.
Hope it help you! :)
So what I ended up doing was:
Authenticate the user making the request via JWT access_token
Get the user's ID and set it to the state option's property
The user is redirected to the google authorization page and choose the account (s)he wants to connect
(S)He gets redirected to my callback url with the state query param having the user's id
Now I just have to get that id, search the user in the database, and set the data I need from req.account which contains the user's openid profile.
var googleScope = ['openid', 'email', 'https://www.googleapis.com/auth/calendar'];
routes.get(
'/google',
auth.authenticate(),
auth.isLoggedIn,
function (req, res, next) {
var _id = '' + req.user._id; // convert to String... _id is an mongoose object
return auth.authorize('google', { session: false, scope: googleScope, passReqToCallback: true, state: _id })(req, res, next)
}
);
routes.get('/callback/google',
function (req, res, next) {
auth.authorize('google', { session: false, scope: googleScope, passReqToCallback: true })(req, res, next);
},
auth.saveUserData()
);
saveUserData= function () {
return function (req, res, next) {
if (req.query.state) {
var _id = req.query.state;
User.findOne({ _id, deleted: false, active: true })
.exec(function (err, user) {
if (err) {
res.send(err);
}
if (user) {
user.auth.google = {
id: req.account.id,
token: req.account.token,
email: (req.account.emails.length ? req.account.emails[0].value : null),
name: req.account.displayName
}
user.save(function (err, data) {
if (err) {
res.send(err);
} else {
res.redirect('/')
}
})
} else {
res.sendStatus(401);
}
})
} else {
res.sendStatus(400)
}
}
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.