The Daily TIL

September 30, 2019 by AndyAndynextjsreactpassportauthentication

You can use Passport.js with Next.js 9

When Next.js 9 was announced, I got really excided about the new API routes feature. In previous versions, you had to create a separate Express.js API app to serve as an authentication backend and deploy it alongside your Next UI app. But now, API routes and UI routes in the same Next app should simplify that project/deployment structure. All I’d have to do is move my backend routes over into my Next 9 app.

But nope… apparently Next’s API handlers are based on Zeit’s Micro HTTP handler framework, not Express. Passport is built for Express handlers at a pretty deep level.

This was frustrating to learn, but after a little denial and persistence, I was able to trick Passport’s middleware into working with Next’s handlers.

Full Example

Here’s the full working Next.js + Passport application if you just want to browse through the code.

Example

Next.js API handlers with middleware

The first difference between Express and Next/Micro is that Micro doesn’t have a real “add middleware” helper. Express has app.use(middleware), but for micro, there really isn’t a first class concept of middleware.

Since passport’s core functionality is executed as middleware (passport.initialize, and passport.session), we’ll have to figure out some way to wrap our Next API route handlers with middleware.

Passport’s middleware assumes your handlers are connect-style. As long as your handlers have a req, res, and next

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next.

…and req and res are compatible with Node.js’s HTTP objects

The req object is an enhanced version of Node’s own request object and supports all built-in fields and methods.

…the middleware should WORK.

Next’s API handers receive objects that ARE compatible with Node.js HTTP objects. You can see that if you look at the typescript type definition for NextApiRequest. You can see that it’s an intersection type that includes http.IncomingMessage, which is the Node.js req type.

But how do we attach the passport middleware without app.use?

Once we understand what middleware actually IS, “functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle”, we can infer that all we really need to do is somehow wrap our Next API handler in layers of middleware, and make sure the inner-most layer calls our API handler as next.

That requires defining your API route handler modules as…

# pseudocode
middleware(req, res, next) ->
  middleware(req, res, next) ->
    handler(req, res)

…instead of just handler(req, res).

but we can’t just pass the outer middleware to Next’s framework (Micro) since it expects the handler module to be a 2 param function with req, res as it’s only params.

So to workaround this, we can define our own middleware-builder function that takes, as input, our API route handler, and returns a Micro-compatible function that internally invokes the middlewares we want, in order, and manually sets the next arguments to either the next middleware in the chain or the actual API route handler we passed in originally… this is easier to see in code:

/**
 * our "wrap with middleware" function
 * @param handle {Micro handler Function}
 * @returns {Micro handler function} wrapped with middleware
 */
export default handler => (req, res) => {
  // Initialize Passport and restore authentication state, if any, from the
  // session. This nesting of middleware handlers basically does what app.use(passport.initialize())
  // does in express.
  passport.initialize()(req, res, /* next */ () =>
    passport.session()(req, res, /* next */ () =>
      // call wrapped api route as innermost handler
      handle(req, res)
    )
  )
}

Once you’ve done this, MOST other authentication components work the same way the would in any other client-server session-based workflow.

Other gotchas

Next HTTP objects are missing some things that Express HTTP objects have

For example, passport middleware needs res.redirect from express.js.

So we need to monkey-patch this function on every request to get the middleware working. We can do this in our “wrap with middleware” function above.

  if (!res.redirect) {
    // passport needs res.redirect:
    // 
    // Monkey-patch res.redirect to emulate express.js's res.redirect,
    // since it doesn't exist in micro. default redirect status is 302
    // as it is in express. https://expressjs.com/en/api.html#res.redirect
    res.redirect = (location: string) => redirect(res, 302, location)
  }
Need to manually call passport.authenticate

Express has syntax for mapping routes to handlers, and this is the syntax used in most passport examples.

// mapping a login route to passport.authenticate in Express
app.post('/login', passport.authenticate('local', { successRedirect: '/',
                                                    failureRedirect: '/login' }));

But Next doesn’t have this. Routing is based on file system paths and handlers are just modules returned from the files at these paths.

So we can’t use a one-liner to map a route to passport.authenticate. No big deal. We just have to manually call passport.authenticate in the handler for the route we choose.

Here’s a full example of what a handler looks like wrapped with middleware and executing authenticate:

import { NextApiResponse, NextApiRequest } from 'next'
import withPassport, { passport } from 'lib/withPassport'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { provider } = req.query
  if (!provider) {
    return { statusCode: 404 }
  }

  passport.authenticate(provider, {
    failureRedirect: '/auth',
    successRedirect: '/',
  })(req, res, (...args) => {})
}

export default withPassport(handler)

References