Skip to main content

Node.js — Passport.js

Familiar middleware-based integration. Works with any Express app.

Install

npm install passport passport-openidconnect express-session

Full Example

Handles login (with PKCE), callback, logout (RP-initiated), and token refresh.

const express = require("express")
const session = require("express-session")
const passport = require("passport")
const { Strategy } = require("passport-openidconnect")

const app = express()

app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
// In production, use a persistent session store (e.g. connect-redis)
cookie: { httpOnly: true, secure: true, sameSite: "lax", maxAge: 30 * 24 * 60 * 60 * 1000 },
}))
app.use(passport.initialize())
app.use(passport.session())

passport.use(
new Strategy(
{
issuer: "https://v1.0account.com",
authorizationURL: "https://v1.0account.com/oauth/authorize",
tokenURL: "https://v1.0account.com/oauth/token",
userInfoURL: "https://v1.0account.com/oauth/userinfo",
clientID: process.env.ZERO_CLIENT_ID,
clientSecret: process.env.ZERO_CLIENT_SECRET,
callbackURL: "https://yourapp.com/auth/callback",
scope: ["openid", "profile", "email", "offline_access"],
pkce: true, // requires passport-openidconnect v0.1.0+
},
(issuer, profile, context, idToken, accessToken, refreshToken, done) => {
// profile.id, profile.emails[0].value, profile.displayName
//
// Custom fields are not on `profile` — passport-openidconnect maps only
// the standard claims. They arrive under a namespaced key on the raw
// userinfo response, which is `profile._json`.
const fields = profile._json?.["https://0account.com/claims/fields"] ?? {}
// A field titled "Pet name" arrives as petName.
const petName = fields.petName

// TODO: upsert user into your database by profile.id
return done(null, {
id: profile.id,
email: profile.emails?.[0]?.value,
displayName: profile.displayName,
petName,
idToken,
accessToken,
refreshToken,
})
},
),
)

passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser((user, done) => done(null, user))

app.get("/auth/login", passport.authenticate("openidconnect"))

app.get(
"/auth/callback",
passport.authenticate("openidconnect", { failureRedirect: "/" }),
(req, res) => res.redirect("/dashboard"),
)

app.get("/auth/logout", (req, res) => {
const idToken = req.user?.idToken
req.logout((err) => {
if (err) return res.status(500).send("logout error")
req.session.destroy(async () => {
if (idToken) {
// Server-to-server: terminate the session on 0account's side without a browser redirect.
await fetch("https://v1.0account.com/oauth/logout", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ id_token_hint: idToken }),
}).catch(() => {})
}
res.redirect("/")
})
})
})

Refreshing the access token — only with offline_access

Access tokens last an hour. Skip this entirely if your app only needs to know who signed in.

// refreshAccessToken — call when req.user.accessToken is near expiry.
// Update req.session.passport.user with the new tokens afterwards.
async function refreshAccessToken(refreshToken) {
const response = await fetch("https://v1.0account.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: process.env.ZERO_CLIENT_ID,
client_secret: process.env.ZERO_CLIENT_SECRET,
}),
})
if (!response.ok) throw new Error("refresh failed")
return response.json()
}
app.listen(3000)

Environment Variables

ZERO_CLIENT_ID=your-0account-client-id
ZERO_CLIENT_SECRET=your-0account-client-secret
SESSION_SECRET=your-random-secret

Going further

This page covers sign-in with 0account and nothing else. For everything the library itself does — middleware, adapters, session storage, multiple providers — see the passport-openidconnect documentation.