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.
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: [
{
strategy: new PassportStrategy(), // 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
.
The passportAuth
adapter adds two API endpoints for each installed
strategy.
With the handler at app/api/auth/[...auth].ts
, it adds the following:
/api/auth/[strategyName]
- URL to initiate login/api/auth/[strategyName]/callback
- Callback URL to complete loginFor example with passport-twitter
strategy, the URLs for Twitter will
be:
/api/auth/twitter
- URL to initiate login/api/auth/twitter/callback
- Callback URL to complete loginYou can determine the strategyName
in the strategy's documentation by
looking for this: passport.authenticate('github')
. So in this case, the
strategyName
is github
.
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
forwarded
or 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: [
/*...*/
],
})
You can access the middleware context and request and response objects by
providing a callback to the passportAuth
adapter. The argument of the
callback is an object with the properties ctx
, req
and res
. You can
then access the session context via ctx.session
or the request object if
you need to include custom parameters in your passport strategies (e.g.,
invitation codes, referal codes).
// app/api/auth/[...auth].ts
import { passportAuth } from "blitz"
import db from "db"
export default passportAuth(({ ctx, req, res }) => ({
successRedirectUrl: "/",
errorRedirectUrl: "/",
strategies: [
{
strategy: new TwitterStrategy({
consumerKey: process.env.TWITTER_CONSUMER_KEY as string,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET as string,
/*...*/
}),
},
],
}))
Note: If your environment variables are not typed, you must add a type assertion to each environment variable when using the callback (as shown in the example above).
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: [
{
strategy: 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(undefined, { publicData })
}
),
},
],
})
Note: The above passport-twitter
example requires your User
prisma
model to have email String @unique
and name String
.
Add a link to your app with URL format of /api/auth/[strategyName]
.
For the above twitter example, the link would be like this:
<a href="/api/auth/twitter">Log In With Twitter</a>
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.
To create a new Blitz session, you need to call the done()
function
from your verify
callback.
done(undefined, 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.
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"))
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
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.
redirectUrl
to the verify
callback resultdone(undefined, {publicData, redirectUrl: '/'})
redirectUrl
query parameter to the "initiate login" urlexample.com/api/auth/twitter?redirectUrl=/dashboard
example.com/api/auth/twitter?redirectUrl=${router.pathname}
passportAuth
config.successRedirectUrl
config.errorRedirectUrl
/
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: "/",
strategies: [
{
authenticateOptions: { scope: "openid email profile" },
strategy: 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 Auth0 app permissions
return done(new Error("Auth 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.