116

I'm trying to establish a login mechanism using node.js, express and passport.js. The Login itself works quite nice, also sessions are stored nicely with redis but I do have some troubles with redirecting the user to where he started from before being prompted to authenticate.

e.g. User follows link http://localhost:3000/hidden is then redirected to http://localhost:3000/login but then I want him to be redirected again back to http://localhost:3000/hidden.

The purpose of this is, if the user access randomly a page he needs to be logged in first, he shall be redirected to the /login site providing his credentials and then being redirected back to the site he previously tried to access.

Here is my login post

app.post('/login', function (req, res, next) {
    passport.authenticate('local', function (err, user, info) {
        if (err) {
            return next(err)
        } else if (!user) { 
            console.log('message: ' + info.message);
            return res.redirect('/login') 
        } else {
            req.logIn(user, function (err) {
                if (err) {
                    return next(err);
                }
                return next(); // <-? Is this line right?
            });
        }
    })(req, res, next);
});

and here my ensureAuthenticated Method

function ensureAuthenticated (req, res, next) {
  if (req.isAuthenticated()) { 
      return next();
  }
  res.redirect('/login');
}

which hooks into the /hidden page

app.get('/hidden', ensureAuthenticated, function(req, res){
    res.render('hidden', { title: 'hidden page' });
});

The html output for the login site is quite simple

<form method="post" action="/login">

  <div id="username">
    <label>Username:</label>
    <input type="text" value="bob" name="username">
  </div>

  <div id="password">
    <label>Password:</label>
    <input type="password" value="secret" name="password">
  </div>

  <div id="info"></div>
    <div id="submit">
    <input type="submit" value="submit">
  </div>

</form>
royhowie
  • 11,075
  • 14
  • 50
  • 67
Alx
  • 6,275
  • 7
  • 32
  • 54
  • Hi I am also doing the same the only difference is when user is successfully log in then i will redirect to welcome.html page with message like'Welcome UserName'. hear UserName will be his/her LoginName. Can you show me how to pass text box value to next page ?? – Dhrumil Shah Aug 11 '13 at 12:16
  • 2
    not knowing much I'd guess simply pass it through. According from my example that might be: res.render('hidden', {title: 'hidden page', username: 'Tyrion Lannister'}); – Alx Aug 11 '13 at 12:35

7 Answers7

126

In your ensureAuthenticated method save the return url in the session like this:

...
req.session.returnTo = req.originalUrl; 
res.redirect('/login');
...

Then you can update your passport.authenticate route to something like:

app.get('/auth/google/return', passport.authenticate('google'), function(req, res) {
    res.redirect(req.session.returnTo || '/');
    delete req.session.returnTo;
}); 
linuxdan
  • 4,476
  • 4
  • 30
  • 41
  • 7
    Worked on the first pass, but I needed to add the following after the redirect in the passport.authenticate route to avoid being taken back to the returnTo address after subsequent logout/login: `req.session.returnTo = null;` See [this question](http://stackoverflow.com/questions/13758207/why-is-passportjs-in-node-not-removing-session-on-logout) for additional info about passport's default logout, which, on examination of source, seems to only clear the session.user, and not the entire session. – Rob Andren Jan 27 '15 at 19:49
  • can i simply pass around a query like ?callback=/profile instead of session – OMGPOP Jun 29 '15 at 03:19
  • @OMGPOP you probably could, but that doesn't sound very secure, is there a reason you don't want to take advantage of the session? – linuxdan Jun 29 '15 at 03:22
  • @linuxdan not at all. but it's just troublesome to extract the redirect link out and put into session, then when user redirect back, assign back the redirect link from the session – OMGPOP Jul 02 '15 at 12:10
  • @linuxdan why is it insecure? – phillipwei Oct 19 '15 at 21:54
  • @phillipwei - if you are passing around authentication related data on the query string, you are giving hackers implementation details of your authentication system. – linuxdan Oct 19 '15 at 22:11
  • @linuxdan but the value here is just the url that the user navigated to originally; I don't think that needs securing does it -- or am I missing something obvious? – phillipwei Oct 20 '15 at 02:08
  • @phillipwei I'm not a security expert, so I can't say for sure, but when it comes to authentication I try to err on the side of caution. Session variables are generally more secure than url parameters since they aren't passed around in the open where a simple packet sniffer can pick them up. Is the redirect path an asset worth securing? Maybe not, but I could imagine it being a problem when combined with other possible security vulnerabilities. – linuxdan Oct 20 '15 at 16:54
  • 6
    Instead of `req.path` I would use `req.originalUrl` since it is a combination of baseUrl and path, see [the documentation](http://expressjs.com/en/api.html#req.originalUrl) – matthaeus Aug 17 '17 at 14:06
  • need to move the delete line to above the other line, but works otherwise – meain May 27 '19 at 12:37
  • URLs are not passed in the clear if the site is secured with TLS. And if your site isn't secured with TLS, most OAuth implementations aren't secure either, so it's a non-issue. On the other hand, I have not been able to find a way to pass along a callback URL to Google that Google will return when it comes back. – Rich Remer Apr 25 '20 at 17:23
89

I don't know about passport, but here's how I do it:

I have a middleware I use with app.get('/account', auth.restrict, routes.account) that sets redirectTo in the session...then I redirect to /login

auth.restrict = function(req, res, next){
    if (!req.session.userid) {
        req.session.redirectTo = '/account';
        res.redirect('/login');
    } else {
        next();
    }
};

Then in routes.login.post I do the following:

var redirectTo = req.session.redirectTo || '/';
delete req.session.redirectTo;
// is authenticated ?
res.redirect(redirectTo);
Community
  • 1
  • 1
chovy
  • 72,281
  • 52
  • 227
  • 295
  • thanks for your reply. Aren't you setting the redirect /account direct in your code? What happens if you have another link let's say /pro_accounts using the same auth.restrict they would be redirected to /account .... – Alx Nov 11 '12 at 22:31
  • 4
    That's true. I always redirect to /account after they login, but you could replace it with `req.session.redirect_to = req.path` – chovy Nov 12 '12 at 05:41
  • 1
    hm.. its not quite I'm looking for. I call ensureAuthenticated to figure out if a user is already authed or not, if not it is redirected to /login. From this view I need a way to get back to /account. Calling req.path within /login gives me simple /login back. Using referer doesn't work either plus its not quite certain if the browser sets the referer properly... – Alx Nov 12 '12 at 10:59
  • 6
    You need to set `req.session.redirect_to = req.path` inside your auth middleware. Then read it and delete it in `login.post` route. – chovy Nov 12 '12 at 18:28
  • 1
    this isn't a great solution for passport which accepts successRedirect as an option at a point that request isn't available – linuxdan Jan 15 '14 at 22:55
  • or you can use req.flash('redirect') – OMGPOP Jun 29 '15 at 11:53
  • BUG: user wants to go to /account but he is not authorized, he is redirected to login and session.redirectTo='/account'. Then he goes to '/' and logins through main page, but session.redirectTo still equals '/account' and he redirects to account but should not – Alexander Danilov Feb 20 '16 at 12:53
17

Take a look at connect-ensure-login, which works along side Passport to do exactly what you want!

Jared Hanson
  • 15,940
  • 5
  • 48
  • 45
7

My way of doing things:

const isAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    return next()
  }
  res.redirect( `/login?origin=${req.originalUrl}` )
};

GET /login controller:

if( req.query.origin )
  req.session.returnTo = req.query.origin
else
  req.session.returnTo = req.header('Referer')

res.render('account/login')

POST /login controller:

  let returnTo = '/'
  if (req.session.returnTo) {
    returnTo = req.session.returnTo
    delete req.session.returnTo
  }

  res.redirect(returnTo);

POST /logout controller (not sure if there is 100% ok, comments are welcome):

req.logout();
res.redirect(req.header('Referer') || '/');
if (req.session.returnTo) {
  delete req.session.returnTo
}

Clear returnTo middleware (clears returnTo from session on any route except auth routes - for me they are /login and /auth/:provider ):

String.prototype.startsWith = function(needle)
{
  return(this.indexOf(needle) == 0)
}

app.use(function(req, res, next) {
  if ( !(req.path == '/login' || req.path.startsWith('/auth/')) && req.session.returnTo) {
    delete req.session.returnTo
  }
  next()
})

This approach have two features:

  • you can protect some routes with isAuthenticated middleware;
  • on any page you can simply click on login URL, and after login return to that page;
deksden
  • 774
  • 6
  • 13
  • There were so many good response here, I was able to use your example and made it work with my project. Thank you! – user752746 Oct 02 '18 at 18:18
6

If you are using connect-ensure-login there is a super-easy, integrated way to do this with Passport using the successReturnToOrRedirect parameter. When used, passport will send you back to the originally requested URL or fallback to the URL you provide.

router.post('/login', passport.authenticate('local', {
  successReturnToOrRedirect: '/user/me',
  failureRedirect: '/user/login',
  failureFlash: true
}));

https://github.com/jaredhanson/connect-ensure-login#log-in-and-return-to

igneosaur
  • 3,268
  • 3
  • 29
  • 44
  • This seems like a duplicate of @jaredhanson's answer – mikemaccana Sep 07 '16 at 15:11
  • Worth adding that `express-session` needs to be configured so that is saves 'uninitialized' cookies: `{ saveUninitialized: true }`. This means that anonymous users will have sessions created for them, and put more pressure against your session store. On the upside, these sessions will store the `returnTo` location and be used after an anonymous user authenticates successfully. – Anton Drukh May 20 '21 at 06:51
1

@chovy and @linuxdan answers have bug with not clearing session.returnTo if user goes to another page after login redirect (thats doesn't require authentication) and logins through there. So add this code to their implementations:

// clear session.returnTo if user goes to another page after redirect to login
app.use(function(req, res, next) {
    if (req.path != '/login' && req.session.returnTo) {
        delete req.session.returnTo
    }
    next()
})

If you do some ajax requests from login page, you can also exclude them.


Another approach is to use flash in ensureAuthenticated

req.flash('redirectTo', req.path)
res.redirect('/login')

And then in GET login

res.render('login', { redirectTo: req.flash('redirectTo') })

In view add hidden field to login form (example in jade)

if (redirectTo != '')
    input(type="hidden" name="redirectTo" value="#{redirectTo}")

In POST login

res.redirect(req.body.redirectTo || '/')

Notice that redirectTo will clear after first GET login with it.

Alexander Danilov
  • 3,038
  • 1
  • 30
  • 35
0

Easiest (and properly) way to achieve this is setting failureRedirect and successRedirect options.

mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Eray
  • 7,038
  • 16
  • 70
  • 120
  • Can you elaborate? I can see `successRedirect` being used on the return route, but then the intended destination (i.e. `returnTo` above) would be lost, right? – Tom Söderlund Dec 22 '17 at 21:02