I'm trying to implement a login/sign-up system similar to StackOverflow's, which is to say:
Both the sign up and login links go to /users/login,
Users click on an OAuth provider (e.g. Google) regardless of whether they're signing up or logging in,
The OAuth callback goes to /users/authenticate if the account doesn't yet exist (page to confirm account creation), OR goes to / if the account already exists.
(I'm adding an administrator account verification step here if the account is new, but not too important for this question.)
I'm not sure I'm going about this correctly. Relevant code below.
See if the profile exists in the database; if not, return the in-memory profile with the status = "new":
passport.use(new GoogleStrategy({
clientID: config.google_client_id,
clientSecret: config.google_client_secret,
callbackURL: "/auth/google/callback"
},
function (accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
db.db.collection("users", function (err, collection) {
if (err) throw err;
collection.findOne({id: profile.id}, function (err, record) {
if (record) return done(null, record);
profile.status = "new";
done(null, profile);
});
});
});
})
);
Pick the redirect route after OAuth based on status:
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/users/login' }),
function (req, res) {
switch (req.user.status) {
case "validated":
res.redirect('/'); break;
case "new":
res.redirect('/users/oauthconfirm'); break;
case "pending":
res.redirect('/users/login'); break;
}
}
);
And finally, the route for confirming a new account:
// app.js
app.get('/users/oauthconfirm', routes.users.oauthconfirm);
// routes/users.js
exports.oauthconfirm = function(req, res) {
db.db.collection("users", function (err, collection) {
if (err) throw err;
collection.insert(req.user, function (err, records) {
if (err) throw err;
res.render('login', {messages: [{status: "success", text:"Thank you. You will receive an e-mail when your account is validated."}]});
});
});
};
What's the "correct" way to do this? I'm pretty sure my verify callback code is inappropriate. Thanks-
How to OpenId with passport-google: OpenId configure strategy
How to Oauth with passport-google-oauth: OAuth configure strategy
By reading your code I can't actually tell which one you are trying to apply. Seems OAuth but then I see a route with /users/openidconfirm which I find unnecesary.
I'll share you 2 links I found:
OpenID vs. OAuth
What's the difference between OpenID and OAuth?
Maybe you could improve your question/trouble so I could elaborate a better answser.
Hope this helps though.
Related
Hi i am working on a project where a user logs in via google account.(localhost)
I have implemented the google signup.
As soon as I log in from my account I am getting the below error.
TokenError: Code was already redeemed.
at Strategy.OAuth2Strategy.parseErrorResponse (c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\lib\strategy.js:298:12)
at Strategy.OAuth2Strategy._createOAuthError (c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\lib\strategy.js:345:16)
at c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\lib\strategy.js:171:43
at c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\node_modules\oauth\lib\oauth2.js:176:18
at passBackControl (c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\node_modules\oauth\lib\oauth2.js:123:9)
at IncomingMessage.<anonymous> (c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\node_modules\oauth\lib\oauth2.js:142:7)
at IncomingMessage.emit (events.js:129:20)
at _stream_readable.js:908:16
at process._tickCallback (node.js:355:11)
My code is as follows(snippet for google login):-
passport.use(new GoogleStrategy(google, function(req, accessToken, refreshToken, profile, done) {
if (req.user) {
User.findOne({ google: profile.id }, function(err, existingUser) {
if (existingUser) {
console.log('There is already a Google+ account that belongs to you. Sign in with that account or delete it, then link it with your current account.' );
done(err);
} else {
User.findById(req.user.id, function(err, user) {
user.google = profile.id;
user.tokens.push({ kind: 'google', accessToken: accessToken });
user.profile.displayName = user.profile.displayName || profile.displayName;
user.profile.gender = user.profile.gender || profile._json.gender;
//user.profile.picture = user.profile.picture || 'https://graph.facebook.com/' + profile.id + '/picture?type=large';
user.save(function(err) {
console.log('Google account has been linked.');
done(err, user);
});
});
}
});
} else {
User.findOne({ google: profile.id }, function(err, existingUser) {
if (existingUser) return done(null, existingUser);
User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
if (existingEmailUser) {
console.log('There is already an account using this email address. Sign in to that account and link it with Google manually from Account Settings.' );
done(err);
} else {
var user = new User();
user.email = profile._json.email;
user.google = profile.id;
user.tokens.push({ kind: 'google', accessToken: accessToken });
user.profile.displayName = profile.displayName;
user.profile.gender = profile._json.gender;
//user.profile.picture = 'https://graph.facebook.com/' + profile.id + '/picture?type=large';
user.profile.location = (profile._json.location) ? profile._json.location.name : '';
user.save(function(err) {
done(err, user);
});
}
});
});
}
}));
I am stuck on it.Please help me out..thanks
The problem is not in your "snippet", look at the routes. It should be absolute path on redirect for google.
router.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '#/signIn' }),
function(req, res) {
// absolute path
res.redirect('http://localhost:8888/#/home');
});
It's known issue, follow this link to other workarounds
https://github.com/jaredhanson/passport-google-oauth/issues/82
I have come across this issue. The exact problem is your route.
app.get('/auth/google/callback', passport.authenticate('google'), (req, res) => {
res.send('get the data');
});
At this point app had got user permission and google send a code to this url. Now what passport does here it took that code and made a request to google for user details and got it from google. Now we have to do something with this details otherwise you will get the error that you have got.
Now we can use serialiseUser and deserialiseUser of passport to save details in cookie and edit one line of above code to go at some url like that.
app.get('/auth/google/callback', passport.authenticate('google'), (req, res) => {
res.redirect('/servey'); // just a url to go somewhere
});
I also had the same problem since few days. What I figured out is, you just need to complete the process. Until now you have only checked whether the user is present in the database or not. If not then you save the user to the database.
However, after this, when the google tries to redirect the user, the code that google+ API sent is already used or say it is no longer available. So when you check the user in your database, you need to serialize the user i.e store the code into your browser in a cookie so that when google redirects the user, it know who the user is. This can be done by adding the code given below.
//add this in current snippet
passport.serializeUser(function(user,done){
done(null,user.id);
});
To use this cookie, you need to deserialize the user. To deserialize, use the code given below.
//add this in current snippet
passport.deserializeUser(function(id,done){
User.findById(id).then(function(user){
done(null, user);
});
});
Also, you are required to start a cookie session and you can do this by adding the below code in your main app.js file.
const cookieSession = require('cookie-session');
app.use(cookieSession({
maxAge: 24*60*60*1000, // age of cookie, the value is always given in milliseconds
keys:[keys.session.cookiekey]
}));
//initialize passport
app.use(passport.initialize());
app.use(passport.session());
Note that you need to require the cookie-session package. Install it using
npm install cookie-session
Also, you require to write absolute URI in the callbackURL property in your google strategy.
I had the same problem.
Reseting client secret from google console solved the problem.
As per the documentation, if I was handling authentication requests like so, I would be able to capture successful attempts.
app.post('/login',
passport.authenticate('local'),
function(req, res) {
// If this function gets called, authentication was successful.
// `req.user` contains the authenticated user.
res.redirect('/users/' + req.user.username);
});
But, like the documentation says:
By default, if authentication fails, Passport will respond with a 401 Unauthorized status, and any additional route handlers will not be invoked. If authentication succeeds, the next handler will be invoked and the req.user property will be set to the authenticated user.
How can I handle the unauthorized login attempt?
I know I can handle it with custom middleware but is there a better way?
You should have a look at the Custom Callback section in passport docs which explains about how to override the built in behavior of handling an authentication request. You can write a custom callback which will serve as the done function that you are invoking from the Strategy.
app.get('/login', function(req, res, next) {
/* look at the 2nd parameter to the below call */
passport.authenticate('local', function(err, user, info) {
if (err) { return next(err); }
if (!user) { return res.redirect('/login'); }
req.logIn(user, function(err) {
if (err) { return next(err); }
return res.redirect('/users/' + user.username);
});
})(req, res, next);
});
Look at the second parameter to the passport.authenticate call, which will serve as the done function that you invoke from the local strategy.
See the done function invoked in the code below, which is the local strategy that you define for passport. You can call the done function with various available parameters like err, user, info set from the strategy according to the response from API call or db operation. These parameters will be processed by the above function definition in the passport.authenticate call.
passport.use(new LocalStrategy(
function(username, password, done) {
/* see done being invoked with different paramters
according to different situations */
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
if (!user.verifyPassword(password)) { return done(null, false); }
return done(null, user);
});
}
));
In my case I didn't want the 401 error (which is the "default behavior"),
and I just wanted a simple redirect if the user wasn't authorized. The simple { failureRedirect: '/login' } worked for me, as explained on the PassportJS documentation (in the "Overview" and the "Redirects" header)
Despite the complexities involved in authentication, code does not
have to be complicated.
app.post('/login', passport.authenticate('local', { successRedirect:'/',
failureRedirect: '/login' }));
In this case, the redirect options override the default behavior. Upon
successful authentication, the user will be redirected to the home
page. If authentication fails, the user will be redirected back to the
login page for another attempt.
The accepted answer gives you more flexibility. But this answer is a bit simpler.
I'm pretty sure "failure" includes the case where there would be no user: if (!user), but I'm not sure if failure includes the error case or not: if (err), as seen in the accepted answer.
I have implemented Passport with passport-local and MongoDB and it is working nicely.
However this is a pure client-side single-loading app and so node is not responsible for the rendering of html. So currently I show a loading a spinner on app load and make a separate call to an api to determine if the user is logged in to conditionally render some stuff:
router.get('/me', function (req, res) {
res.send(req.isAuthenticated() ? {} || 401);
});
Since passport already authenticates my routes and calls deserializeUser this seems pointless - I need a way to pass an extra piece of info (in the cookie?) stating that the user is authed, I am guessing in deserializeUser?
server.use(session({secret: settings.sessionSecret}));
server.use(passport.initialize());
server.use(passport.session());
....
passport.use(new LocalStrategy(
localOpts,
function(email, password, done) {
User.findOne({
email: email,
activated: true
}, function (err, user) {
....
});
}
));
passport.serializeUser(function (user, done) {
done(null, user._id);
});
passport.deserializeUser(function (id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
Note that the two cookies that get created when sign in is successful:
express:sess
express:sess.sig
When it detects the presence of these cookies it seems to just call deserializeUser hence why I think I could possibly communicate to the client the user is authed there, or otherwise on sign in inside passport.use middleware?
It turns out that I can simply add a middleware after the passport.session. I was concerned that req.isAuthenticated would fire off another query to the db but it doesn't:
server.use(function(req, res, next) {
res.cookie('isAuthenticated', req.isAuthenticated());
next();
});
This has not been noticed before, because for client-server communications, nothing requiring authentication is happening via XHR (as of yet). In implementing some integration tests, I am creating realy requests node-xmlhttprequest to a real instance of the application. User is being authenticated in the first request, and in theory the user's unique identifier and some other pertinent information is stored in session (as I've said, this works just fine for real clients, who do not need to confirm their identity over XHR). For some reason, even though subsequent requests are being fired with the exact same session ID, I am not able to retrieve the session in subsequent requests, and thus can't see that the user is authenticated, and this leads to failing tests where the expected behaviour does not happen, and HTTP 401 UNAUTHORIZED is filling up the terminal.
I've seen a couple of questions which look sort of like this, but that "same session ID" thing does not seem to be present among them.
Do I need to manually add Set-Cookie headers to all XHR requests? That's terrible, there's got to be a better way!
So people like source code, here's some from the test, firing these requests:
// Ensure logged in before sending request, to verify that authorized
// users encounter the expected behaviour.
jQuery.post(domain+'/login', {email:'test#express.app', password:'12345678'},
function(data,x,y){
data.should.equal("true")
jQuery.ajax({url:domain+'/event', type:"POST",
name: "TestEvent", location: "TestVille",
startDate: new Date('2013-09-01'), startTime: '6:45 pm',
description: "Test Event #1", success: state.success,
error: state.error, completed: state.completed
})
})
Sign in happens, user ID gets written into session (`req.logIn() should do this, and it does not report any failures), serialization of the user ID does not report any failures, and I am extremely confused as to why subsequent requests using the same session ID are unable to find the serialized user ID.
I am generally not involved with web development, so this may be obvious to some people, but I've been searching for an answer all day and have simply not been able to find one. I'd appreciate any pointers, and am happy to provide as much code as I'm able to, to illustrate what the problem is.
A few additional points of code which may be pertinent:
Serialization/deserialization of user ID (currently implemented in the simplest possible
manner -- This is very early in the initiallization of middleware, after initiallizing passport and passpot.session(). And this works perfectly well for non-XHR requests)
// Serialize user for passport session-store
// Currently only serializing 'user_id'
passport.serializeUser(function(user, done) {
done(null, user._id)
})
// Deserialize user from session-store to provide
// access to the User instance
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user)
})
})
Authentication of users via the local strategy:
passport.use(new LocalStrategy({
usernameField: "email", passwordField: "password",
passReqToCallback: true, failureFlash: true
},
function(req, email, password, done) {
User.findByEmail(email, function(err, user) {
if(user) {
if(err) console.log(err)
if(user instanceof UserLocal) {
user.verifyPassword(password, function(err, match) {
if(err) console.log(err)
if(!match) {
return done(err, false,
"Username or password is incorrect.")
}
return done(null, user)
})
} else {
var msg = "Account registered under " + user.providerName()
+ ", please login using " + user.providerName()
return done(err, false, msg)
}
} else {
return done(err, false, "Username or password is incorrect.")
}
})
})
And finally the requests which write to session in the first place:
function loginHelper(req, res, next) {
passport.authenticate('local', {
failureFlash:true,
failureRedirect: false,
successRedirect: false
},
function(err, user, info) {
req.logIn(user, function(err) {
if(!err) err = {}
if(req.xhr) {
console.log(req.session)
res.status(200).end(err.message || "true")
} else {
if(err.message) req.flash('error', err.message)
else res.redirect('/')
}
})
})(req, res, next)
}
I know there are some weird things like sending a status of 200 regardless of the login status, but I can confirm that the serialized user id is written to session on the initial login XHR request, and that it's not deserialized on subsequent XHR requests.
As I believe I mentioned, I am relatively inexperienced in this area, and could very much use a boost. Any assistance whatsoever would be much appreciated, thanks in advance.
That's help me
First
app.use(express.static(__dirname + '/public', { maxAge: oneDay }));
and update functions to do not trigger database
passport.serializeUser( (user, done) => {
var sessionUser = { id: user.dataValues.id, fio: user.dataValues.fio, email: user.dataValues.localemail, role: user.dataValues.role, login: user.dataValues.login, position: user.dataValues.position }
done(null, sessionUser)
})
passport.deserializeUser( (sessionUser, done) => {
// The sessionUser object is different from the user mongoose collection
// it's actually req.session.passport.user and comes from the session collection
done(null, sessionUser)
})
https://www.airpair.com/express/posts/expressjs-and-passportjs-sessions-deep-dive
I've successfully implemented passport-local into my Express/Mongoose web-app but I'm having trouble figuring out how to render a failed login message properly.
Here's my login route:
app.get('/login', function(req, res) {
res.render('user/login', {
});
});
With a route like that how am I supposed to report an invalid login? If the login is successful it will write the id/username to the req.user object but that doesn't help me in the "GET /login" route because if it's successful you will get redirected to the page you want to go.
That means req.user will always be undefined when you GET the login page.
I want to be able to write out a message saying something like 'yo, invalid login!' when the following things happen:
The user does not exist.
The password supplied does not match but the user existed.
I might want to output a different message depending on what occurred.
When I implemented the LocalStrategy I used this code:
passport.use(new LocalStrategy({
usernameField: 'email'
},
function(email, password, fn) {
User.findOne({'login.email': email}, function(err, user) {
// Error was thrown.
if (err) {
return fn(err);
}
// User does not exist.
if (!user) {
return fn(null, false);
}
// Passwords do not match.
if (user.login.password != utility.encryptString(user.login.salt + password)) {
return fn(null, false);
}
// Everything is good.
return fn(null, user);
});
}
));
As you can see there are some problems but this is how the author of PassportJS set up his application. How are we supposed to access what the Strategy returns?
Like if it throws an error, what am I supposed to even call to get access to err?
Thanks.
In the latest version of Passport, I've added support for flash messages, which make it easy to do what you are asking.
You can now supply a third argument to done, which can include a failure message. For example:
if (user.login.password != utility.encryptString(user.login.salt + password)) {
return fn(null, false, { message: 'yo, invalid login!' });
}
Then, set failureFlash to true as an option to authenticate().
passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login',
failureFlash: true });
In this case, if authentication fails, the message will be set in the flash, ready for you to render it when you display the login page again.
Custom callbacks are also perfectly fine. The built in options just make it simpler to accomplish common tasks.
Also, I'm curious: you mention that there are problems with the sample. What do you think should be improved? I want to make the examples as good as possible. Thanks!
(For more details, see this comment on issue #12).
You can use the custom callback or middleware functionality to have more control. See the Authentication section of the guide for examples.
For example, a custom callback might look like:
app.get('/login', function(req,res,next) {
passport.authenticate('local', function(err,user) {
if(!user) res.send('Sorry, you\'re not logged in correctly.');
if(user) res.send('Skeet skeet!');
})(req,res,next);
});
Alternatively, you could always redirect both responses:
app.get('/login',
passport.authenticate('local', { successRedirect: '/winner',
failureRedirect:'/loser' }));
Or redirect the failure with simple middleware:
app.get('/login', ensureAuthenticated,
function(req,res) {
// successful auth
// do something for-the-win
}
);
// reusable middleware
function ensureAuthenticated(req,res,next) {
if(req.isAuthenticated()) {return next();}
res.redirect('/login/again'); // if failed...
}