Passport.js

Blitz provides an adapter that lets you use an existing

Passport.js authentication strategy.

Currently only passport strategies that use a

verify callback are supported. In the Twitter example below, the second argument to TwitterStrategy() is the verify callback.

Setup

1. Add the Passport.js API Route

Add a new api route at

app/api/auth/[...auth].ts with the following contents.

// app/api/auth/[...auth].ts
import {passportAuth} from "blitz"
import db from "db"
export default passportAuth({
successRedirectUrl: "/",
errorRedirectUrl: "/",
strategies: [
// Provide initialized passport strategy here
],
})

If you need, you can place the api route at a different path but the filename must be

[...auth].js or [...auth].ts.

URLs

The

passportAuth adapter adds two API endpoints for each installed strategy.

With the handler at

app/api/auth/[...auth].ts, it adds the following:

  1. /api/auth/[strategyName] - URL to initiate login
  2. /api/auth/[strategyName]/callback - Callback URL to complete login

For example with

passport-twitter strategy, the URLs for Twitter will be:

  1. /api/auth/twitter - URL to initiate login
  2. /api/auth/twitter/callback - Callback URL to complete login

You can determine the

strategyName in the strategy's documentation by looking for this: passport.authenticate('github'). So in this case, the strategyName is github.

SSL Proxy Configuration

You may need to set

secureProxy option to true in case your app is located behind SSL proxy (Nginx). Proxy should be set to manage x-forwarded-proto header correctly.

// app/api/auth/[...auth].ts
import {passportAuth} from "blitz"
import db from "db"
export default passportAuth({
successRedirectUrl: "/",
errorRedirectUrl: "/",
secureProxy: true,
strategies: [/*...*/],
})

2. Add a Passport Strategy

Add a strategy to the

strategies array argument for passportAuth in the API route, and then follow the strategy's documentation for setup.

Here's an example of adding

passport-twitter.

Note that the

callbackURL uses the callback endpoint as described above (/api/auth/twitter/callback)

import {passportAuth} from "blitz"
import db from "db"
import {Strategy as TwitterStrategy} from "passport-twitter"
export default passportAuth({
successRedirectUrl: "/",
errorRedirectUrl: "/",
strategies: [
new TwitterStrategy(
{
consumerKey: process.env.TWITTER_CONSUMER_KEY,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
callbackURL:
process.env.NODE_ENV === "production"
? "https://example.com/api/auth/twitter/callback"
: "http://localhost:3000/api/auth/twitter/callback",
includeEmail: true,
},
async function (_token, _tokenSecret, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
// This can happen if you haven't enabled email access in your twitter app permissions
return done(new Error("Twitter OAuth response doesn't have email."))
}
const user = await db.user.upsert({
where: {email},
create: {
email,
name: profile.displayName,
},
update: {email},
})
const publicData = {userId: user.id, roles: [user.role], source: "twitter"}
done(null, {publicData})
},
),
],
})

Note: The above

passport-twitter example requires your User prisma model to have email String @unique and name String.

Using a Strategy

Upon successful authentication with the third-party, the user will be redirected back to the above auth API route. When that happens, the

verify callback will be called.

When the

verify callback is called, the user has been authenticated with the third-party, but a session has not yet been created for your Blitz app.

Create a Session

To create a new Blitz session, you need to call the

done() function from your verify callback.

done(null, result)

where

result is an object of type VerifyCallbackResult

export type VerifyCallbackResult = {
publicData: PublicData
privateData?: Record<string, any>
redirectUrl?: string
}

The Blitz adapter will then call

session.create() for you and redirect the user back to the correct place in your application.

Return an Error

If instead, you want to prevent creating a session because of some error, then call

done() with an error as the first argument. The user will then be redirected back to the correct location.

return done(new Error("it broke"))

Showing the Error to the User

Any error during this process will be provided as the

authError query parameter.

For example with

errorRedirectUrl = '/' and done(new Error("it broke")), the user will be redirected to:

/?authError=it broke

Post Authentication Redirects

There are four different ways to determine the redirect URL where a user should be sent after they are authenticated. They are listed here in order of priority. A URL provided with method #1 will override all other URLs.

  1. Add redirectUrl to the verify callback result
    • Example: done(null, {publicData, redirectUrl: '/'})
  2. Add a redirectUrl query parameter to the "initiate login" url
    • Example: example.com/api/auth/twitter?redirectUrl=/dashboard
    • Example: example.com/api/auth/twitter?redirectUrl=${router.pathname}
  3. Via the config passed to passportAuth
    • If success, it will use config.successRedirectUrl
    • If error, it will use config.errorRedirectUrl
  4. If none of the above are provided, it will redirect to /

Note: If there is an error, methods #1 and #2 will override

config.errorRedirectUrl

This should give you maximum flexibility to do anything you need. If this doesn't meet your needs, please open an issue on Github!

authenticateOptions

Some strategies have to call an option like

scope or successMessage inside the passport.authenticate() method. Add these options to the passportAuth object like this:

import {passportAuth} from "blitz"
import db from "db"
import {Strategy as Auth0Strategy} from "passport-auth0"
export default passportAuth({
successRedirectUrl: "/",
errorRedirectUrl: "/",
authenticateOptions: {scope: "openid email profile"},
strategies: [
new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL:
process.env.NODE_ENV === "production"
? "https://example.com/api/auth/auth0/callback"
: "http://localhost:3000/api/auth/auth0/callback",
},
async function (_token, _tokenSecret, extraParams, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
// This can happen if you haven't enabled email access in your twitter app permissions
return done(new Error("GitHub OAuth response doesn't have email."))
}
const user = await db.user.upsert({
where: {email},
create: {
email,
name: profile.displayName,
},
update: {email},
})
const publicData = {
userId: user.id,
roles: [user.role],
source: "auth0",
}
done(undefined, {publicData})
},
),
],
})

Note: Without the

authenticateOptions the profile parameter inside the verify function would not contain any values.

Idea for improving this page?Edit it on Github