Dynamic Auth Redirects With PassportJS

Daniel Golant - Oct 11 '18 - - Dev Community

If you've spent much time programming, you've probably worked with authentication. If you're working with Node, that most likely means you've worked with Passport. Passport is a wonderful tool that's saved millions —if not billions- of developer hours, and it boasts a robust ecosystem of plugins for just about any provider or backend you could imagine. That being said, as a highly customizable library, documentation and community answers for niche use-cases aren't necessarily easy to come by. When I came across the need to get data from one end of an authentication loop through to the other, I found it surprisingly hard to find documentation on how to do that.

I am working on a project I have been tinkering at on-and-off with for the last year. When I first offered it up to a small group of testers, the most common request —much to my chagrin- was to add more authentication options. I was not surprised by how often respondents asked for the feature, but I was hoping against hope that my quickly-rolled Google web auth would somehow be good enough, because I had consciously cut some corners by deferring all auth to Google in the hopes of avoiding having to deal with it. Seeing as how it was the number one requested change though, I started on the long, slow journey of fixing my auth.

Eventually, I came across a need to pass data through the auth loop and back to my server. In my use-case, I wanted to let the calling page influence where the user was redirected after they successfully authorized the application. The solution I came up with was to pass the destination URI as a query param on the authentication request.

I struggled with how to implement it for much longer than I'd like to admit, primarily because of the lack of great documentation on how to pass data back to my callback route. Most answers pointed to the passReqToCallback option, but that ended up being a red herring. I wrestled with the problem until I stumbled upon this answer by Github user itajajaja, which detailed why I had previously failed in using the state parameter.

To start, you'll want to set up your Passport config as you usually would, in this example we'll be using passport-github.

    const GitHubStrategy = require('passport-github').Strategy;
    const express = require('express');
    const User = require('PATH/TO/USER/MODEL');
    const app = express();

    passport.use(new GitHubStrategy({
        clientID: GITHUB_CLIENT_ID,
        clientSecret: GITHUB_CLIENT_SECRET,
        callbackURL: "http://process.env.HOST:4000/auth/github/callback"
      },
      function(accessToken, refreshToken, profile, cb) {
        User.findOrCreate({ githubId: profile.id }, function (err, user) {
          return cb(err, user);
        });
      }
    ));

    // Aaaand wherever you define your router instance

    app.get('/auth/github',
      passport.authenticate('github'));

    app.get('/auth/github/callback', 
      passport.authenticate('github', { failureRedirect: '/login' }),
      function(req, res) {
        // Successful authentication, redirect home.
            res.redirect('/');
    });

    app.listen(4000);

So far, we have an Express instance that sends an authentication request to Github when a user sends a GET to host:4000/auth/github, and we have passport configured to "return" the response of that request to the configured callback route after running it through the verification function.

Unfortunately, the default setup leaves us with a redirect scheme that is fairly static. If I wanted to redirect to a path based off some attribute of the user, or based off the requesting path, I could maybe set some cases in a switch statement. As the number of calling routes and providers increases though, this approach becomes unsustainable, especially because much of it would rely on modifying state external to the request itself.

Luckily, passport provides us with a state parameter that acts as a great medium for transferring data through the auth loop. We can use it by updating our /auth routes like so:

    app.get(`/auth`, (req, res, next) => {
        const { returnTo } = req.query
        const state = returnTo
            ? Buffer.from(JSON.stringify({ returnTo })).toString('base64') : undefined
        const authenticator = passport.authenticate('github', { scope: [], state })
        authenticator(req, res, next)
    })

    app.get(
        `/auth/callback`,
        passport.authenticate('github', { failureRedirect: '/login' }),
        (req, res) => {
            try {
                const { state } = req.query
                const { returnTo } = JSON.parse(Buffer.from(state, 'base64').toString())
                if (typeof returnTo === 'string' && returnTo.startsWith('/')) {
                    return res.redirect(returnTo)
                }
            } catch {
                // just redirect normally below
            }
            res.redirect('/')
        },
    )
Props to @itajajaja for this code

Above, we extract a parameter called returnTo from the request query and Base64 encode it before attaching it to the auth request's options. When the request comes back, we extract the state from the returning request's parameters, and then decode and extract the returnTo value from that. At this point we validate returnTo's value and redirect to the intended destination.

Easy as pie, right? Now, you can easily do even more than that. For example, in my app, I also pass additional parameters through the state:

const authenticate = (options) => {
  return (req, res, next) => {
    const { redir, hash } = req.query;
    const state = redir || hash 
? new Buffer(JSON.stringify({ redir, hash })).toString('base64') : undefined;
    const authenticator = passport.authenticate(options.provider, {
      state,
      // etc
    });
    authenticator(req, res, next);
  };
};


const callback = (provider, failureRedirect) => [
  passport.authenticate(provider, { failureRedirect: failureRedirect || defaultFailureRedirect }),
  async (req, res) => {
    if (req.isAuthenticated()) {
      const { state } = req.query;
      const { redir, hash } = JSON.parse(new Buffer(state, 'base64').toString());
      const user = (await User.findByID(req.user.id))[0];
      if (typeof returnTo === 'string' && returnTo.startsWith('/')) {
        if (hash) {
          User.set(hash)
        }
        return res.redirect(returnTo)
      }
    }
  }
]

Above, if we pass the hash parameter through on the original request, we are able to update our user with the data we passed before redirecting them back to their destination. Ta-da! Like that we can easily track where users came from when they last logged in, unify login and signup routes while properly redirecting, etc.

Can you think of any other ways passing data through the auth loop could be used? Let me know in the comments! If you enjoyed this post, feel free to follow me here on dev.to, or on Twitter.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .