I am using nodejs (express) with mongodb and I am trying to figure out how cookies work. I am currently able to let a user login and authenticate it. How do I bring cookies into play and how do I use cookies to query mongodb for the user's info to pull it onto the next page and pages after that, once they login.
Currently I have a route file that posts the login request and then redirects based on success to a userProfile page, I want to include user specific details on that page and then be able to show user other pages and have him return to his unique pages again while querying.
UPDATED CODE: (Can cookie be called the way it is called in the updated code?)
login post route file
exports.loginPost = function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) { return next(err) }
if (!user) { return res.redirect('loginError'); }
req.logIn(user, function(err) {
if (err) { return next(err); }
res.cookie('name', req.params.email, { expires: new Date(Date.now() + 900000), httpOnly: true });
return res.redirect('userProfile');
});
})(req, res, next);
};
read this first What does middleware and app.use actually mean in Expressjs?
then use cookieparser and cookiesession
app.use(express.cookieParser('yoursecretkeyhere'));
app.use(express.cookieSession();
and split your above function into an authentication middleware method and an authenticate POST handler.
Your authentication middleware just needs to check whether the session has an Authenticated flag and if not redirect to your login page. (if this is part of a single page app, just return a 401 and ask for credentials in your browser)
Your authenticate POST handler then checks the posted username and password credentials (or other credentials against a 3rd party api) and sets the session.Authenticated flag followed by a redirect.
Related
I'm protecting an API in my web app using passport. Before hitting the resource, I want the user(from oauth client) to login/authorize first.
Have tried both LocalStrategy and BasicStrategy - their implementation are almost 100% the same, just look for a user by email and verify password. If I use BasicStrategy, the browser will bring up a dailog saying 'Authentication Required' and ask for username & password. However if I use LocalStrategy, it just says 'Unauthorized', and no chance to login.
So my questions are:
How does browser decide to bring up the login dialog, or is it done by BasicStrategy?
Is it possible to show a login page with some UI, instead of the simple dialog?
Note that it's part of OAuth process so I don't really want to redirect to the login page.
Passport is used only for authentication. Later after one needs to maintain session , to check if user is logged in or not.
So you can make a middleware call before every route.
Middlleware checking if user is logged in or not .
'use strict';
var express = require('express');
module.exports = {
isLoginCheck : function (request, response, next) {
if(!request.session.user && request.path != '/login'){
response.redirect('/login');
}else{
next();
}
},
};
In routes file import the middleware,
var express = require('express'),
indexController = require('./../controller/index'),
middleware = require('./../middleware/index'),
passport = require('passport'),
router = express.Router();
router.get('/addUser', indexController.addUser);
router.post('/saveUser', indexController.saveUser);
router.use(middleware.isLoginCheck);
router.get('/', indexController.index);
router.get('/login', indexController.login);
router.post('/login', function(request, response, next){
passport.authenticate('local', function(err, user, info) {
if (err) {
return next(err);
}
if (!user) {
var message = "Invalid credentials";
return response.render('login',{message: info.message, userLoggedIn : null});
}
request.logIn(user, function(err) {
if (err) { return next(err); }
request.session.user = request.user;
response.redirect('/userList');
});
})(request, response, next);
});
Note : in the success we are saving the verified user in session , which will persist till the server is running. Middleware above we are checking for the session object.
Turned out the Authentication dialog is the sequela of response header:
The browser detects that the response to my call on the XMLHttpRequest object is a 401 and the response includes WWW-Authenticate headers. It then pops up an authentication dialog asking, again, for the username and password.
Here's a very helpful post.
As for the standalone signin page, because it's part of OAuth process, it's better to use express's redirect.
I have a Passport js local signup strategy that makes use of the successRedirect and failureRedirect methods. The problem is, if there is an error with the signup process, Passport will simply redirect back to the signup form, with no data in the form.
app.post('/signup', passport.authenticate('local-signup', {
successRedirect: '/signup/avatar', // redirect to the secure profile section
failureRedirect: '/signup', // redirect back to the signup page if there is an error
failureFlash: true // allow flash messages
}));
I have the following condition inside my local signup strategy that will send a failure message to the user if the two provided passwords do not match.
if(req.body.password != req.body.confirm_password){
console.log('Passwords do not match');
return done(null, false, req.flash('signupMessage', 'Your passwords do not match'));
}
Along with the flash message, I want to send data back to the /signup route and re-populate the form with the data the user has provided.
Is there a way to send the form data back to the browser on failure?
The answer was much simpler than I expected.
The validation should happen before the data is sent to Passport js, then if the data is not valid, the /signup template can be re-rendered and the data passed back.
The solution was in this question: NodeJS express-validator with passport
Sorry, after our comments I understand what you're asking a little better now. You need to make use of a custom callback in your passport code. The passport site gives this example:
app.get('/login', function(req, res, next) {
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);
});
Through a closure here, your callback has access to both the request and the response, so when you deal with your error state, you can invoke code to update the response from the originating request (e.g. pulling in the specified username or whatever it is you wish to re-populate the sign-up screen with).
http://passportjs.org/docs/authenticate
I have implemented the OAUth 2.0 flow as single authentication method for my express application.
In real situation, once a user connected using OAuth, a session is created and accessible server-side in req.session.passport.
In this session, there is the OAuth access token that is required to perform authenticated requests on my REST API. To be able to do so, I wrote a middleware that copies the token from req.session.passport to the Authorization header.
app.use(function(req, res, next) {
if (req.session.passport) {
req.headers['Authorization'] = req.session.passport.user.accessToken;
}
next();
});
Now, I would like to test this middleware without going through the entire oauth flow. Ideally, I would like to create a session manually and inject test data inside req.session.passport, then call any protected endpoint of my API to check that it can correctly perform authenticated requests.
I tried to manually set data inside req.session in the following test
it('Should be able to make an authenticated request with session data',
function(done) {
var req = request(app).get('/api/SomeProtectedEndpoint');
req.session = {
passport: {
user: {
accessToken : 'eade123d3ffwhatever'
}
}
}
req.expect(200, function(err, res){
if (err) return done(err);
done();
});
});
But if I display the contents of req.session inside my middleware, the manual data is gone. I believe it is removed by express-session.
How can I manually inject session data in a request for testing purposes ?
Background
I have a MEAN application with CRUD capabilities fully tested with postman. I have been trying to persist login for quite some time now with no luck. I have read and tried the following
passport docs
toon io's blog about login
Express session
Scotch io, node auth made easy
Plus a buch of other light reading materials (Lots of SO questions)
But I have only been able to register and log a user in, not persist login with a session.
My App
Here is a link to the full github repo (if you are looking for the latest changes check develop branch)
My Understanding of Auth/Login
Here is my understanding of user login with code examples from my project and screenshot of postman results as well as console logs.
Passport setup
I have the following auth.js file, it configs passport
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
module.exports = function(app, user){
app.use(passport.initialize());
app.use(passport.session());
// passport config
passport.use(new LocalStrategy(user.authenticate()));
passport.serializeUser(function(user, done) {
console.log('serializing user: ');
console.log(user);
done(null, user._id);
});
passport.deserializeUser(function(id, done) {
user.findById(id, function(err, user) {
console.log('no im not serial');
done(err, user);
});
});
};
This gets called in the server file like
//code before
var user = require('./models/user.js');
var auth = require('./modules/auth.js')(app, user);
// code after
Routing for login
In my routes I have the login route as follows
router.post('/login', function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json({
err: info
});
}
req.logIn(user, function(err) {
if (err) {
return res.status(500).json({
err: 'Could not log in user'
});
}
res.status(200).json({
status: 'Login successful!'
});
});
})(req, res, next);
});
This route works as tested with postman. I enter the details 'joe' and 'pass' and get the following response.
When this route is hit we can also see in the console that the user is serialized.
So what next?
This is where I get lost. I have a few questions.
Is the user now in a session on my server?
Should I send the req.session.passport.user back to the client?
Do I need the session ID on all future requests?
Testing the Session
I have a second route setup for testing the session it is as follows
router.get('/checkauth', passport.authenticate('local'), function(req, res){
res.status(200).json({
status: 'Login successful!'
});
});
The part passport.authenticate('local') (I thought) is there to test if the user session exists before giving access to the route but I never get a 200 response when I run this, even after a login.
Does this route expect a req.session.passport.user passed in the head or as a data argument on a http request that requires auth?
If I missed anything or am understanding something wrong please tell me, any input is appreciated. Thanks all.
Is the user now in a session on my server?
No, You need to use the express-session middleware before app.use(passport.session()); to actually store the session in memory/database. This middleware is responsible for setting cookies to browsers and converts the cookies sent by browsers into req.session object. PassportJS only uses that object to further deserialize the user.
Should I send the req.session.passport.user back to the client?
If your client expects a user resource upon login, then you should. Otherwise, I don't see any reason to send the user object to the client.
Do I need the session ID on all future requests?
Yes, for all future requests, the session id is required. But if your client is a browser, you don't need to send anything. Browser will store the session id as cookie and will send it for all subsequent requests until the cookie expires. express-session will read that cookie and attach the corresponding session object as req.session.
Testing the Session
passport.authenticate('local') is for authenticating user credentials from POST body. You should use this only for login route.
But to check if the user is authenticated in all other routes, you can check if req.user is defined.
function isAuthenticated = function(req,res,next){
if(req.user)
return next();
else
return res.status(401).json({
error: 'User not authenticated'
})
}
router.get('/checkauth', isAuthenticated, function(req, res){
res.status(200).json({
status: 'Login successful!'
});
});
As #hassansin says you need to use a middleware that implement session management. The passport.session() middleware is to connect the passport framework to the session management and do not implement session by itself. You can use the express-session middleware to implement session management. You need to modify your auth.js in the following way
var passport = require('passport');
var session = require('express-session');
var LocalStrategy = require('passport-local').Strategy;
module.exports = function(app, user){
app.use(session({secret: 'some secret value, changeme'}));
app.use(passport.initialize());
app.use(passport.session());
// passport config
passport.use(new LocalStrategy(user.authenticate()));
passport.serializeUser(function(user, done) {
console.log('serializing user: ');
console.log(user);
done(null, user._id);
});
passport.deserializeUser(function(id, done) {
user.findById(id, function(err, user) {
console.log('no im not serial');
done(err, user);
});
});
};
Notice that in this case the session engine is using the in memory store and it didn't work if you scale your application and apply load balancing. When you reach this development state something like the connect-redis session store will be needed.
Also notice that you need to change the secret value used on the session midleware call and use the same value on all application instances.
As per the passport documentation, req.user will be set to the authenticated user. In order for this to work though, you will need the express-session module. You shouldn't need anything else beyond what you already have for passport to work.
As far as testing the session, you can have a middleware function that checks if req.user is set, if it is, we know the user is authenticated, and if it isn't, you can redirect the user.
You could for example have a middleware function that you can use on any routes you want authenticated.
authenticated.js
module.exports = function (req, res, next) {
// if user is authenticated in the session, carry on
if (req.user) {
next();
}
// if they aren't redirect them to the login page
else {
res.redirect('/login');
}
};
controller
var authenticated = require('./authenticated');
router.get('/protectedpage', authenticated, function(req, res, next) {
//Do something here
});
I don't know of a way to check all existing sessions, but Passport is handling the issuing of session ids. Try checking that you have req.user on your test endpoint after logging in
Users visiting http://localhost/login are instantly redirected to Facebook for confirmation of application usage. Once authorized, Facebook contacts http://localhost/login?code= with a special code that allows the server to obtain the user's information such as their name and gender.
Express().get('/login', Passport().authenticate('facebook', {
failureRedirect: 'http://localhost/',
}), function(req, res) {
database.saveData(req.user, **randomlyGeneratedHash**);
res.cookie('session', **randomlyGeneratedHash**);
res.end();
});
This works as expected, but when authenticated users visit the /login in succession, the whole process is repeated and they get a new cookie.
Is there a way that I can run some code inbetween Express and Passport, to stop Passport from redirecting to Facebook if the user has a valid cookie already?
You can use something similar to ensureAuthenticated on your /login route:
var CheckIfAlreadyLoggedIn = function(req, res, next) {
if (req.isAuthenticated()) {
return res.redirect('/'); // or where you want them to go
}
next();
};
Express().get('/login', CheckIfAlreadyLoggedIn, Passport().authenticate('facebook', ...));
This would redirect users that are already logged in back to / when they try to access /login.