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)