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 brokeThere 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=/dashboardexample.com/api/auth/twitter?redirectUrl=${router.pathname}passportAuthconfig.successRedirectUrlconfig.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!
authenticateOptionsSome 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.