DEV Community

Cover image for Dynamic Auth Redirects With PassportJS
Daniel Golant
Daniel Golant

Posted on • Updated on

Dynamic Auth Redirects With PassportJS

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.

Top comments (10)

Collapse
 
anabella profile image
anabella

Hey! Great post! :) I've been working with passport for a while and trying to understand it fully and not just "use" it.

But anyway, I'll tell you how to get syntax highlighting if you tell me how to get that Props to @itajajaja for this code alt text on code blocks.

If you do:
js codeblock example

It'll get nice JS colors which (at least for me) make it a lot easier to read. It works for CSS and HTML too. And probably others 😉

Cheers!

Collapse
 
dangolant profile image
Daniel Golant

Thanks! I can't believe i totally missed that.

Captions can be anywhere I think, you just need to wrap the statement in

. I added it to the markdown docs a while back because I would constantly struggle with re-formatting my stuff from medium.

Collapse
 
anabella profile image
anabella

If I wrap them in what 😱

You added it to the markdown docs here? I struggle too with the medium "translation" 😅

Thread Thread
 
dangolant profile image
Daniel Golant

how many L's can I take in a thread 😂

<figcaption>
Whatever you wanna write, I guess it works in comments too 
</figcaption>

It's in the "Markdown" tab of the writing guide when you draft a post (unless it was taken out)

Collapse
 
demostenes1509 profile image
Maximiliano Ezequiel Carrizo

Your article was a life saver to me ... congrats !

P.S. I don't understand why nobody complains about fixed callback url ...

Collapse
 
dangolant profile image
Daniel Golant

Glad it helped, I thought about linking it in the passport wiki. Having recently worked with OmniAuth in Rails, I’m starting to think that OAuth being complicated leads to difficulty in documenting the “user”-facing parts of OAuth libs.

Collapse
 
koribrus profile image
Kori Brus

Thanks Daniel. This is the solution I need, but I'm missing a step somewhere.

My req.query property from within /auth is returning an empty object. Likewise, returnTo is undefined. How should I be getting this data into the flow?

I'm using the spotify strategy with the a direct call to the /auth route from a React frontend.

Collapse
 
jlmonroy13 profile image
Jorge Luis Monroy Herrera

Great post man!! Thanks!

Collapse
 
szabi84 profile image
szabi84

Hi! Great article. It helped me a lot!
Thanks!

Collapse
 
sancho1952007 profile image
Sancho Godinho

Thank you so much bro 💪