Auth (Auth0)
Pulse integrates with Auth0 — and any OAuth 2.0 provider — using plain HTTP redirects and a token exchange. No client-side auth SDK is required. Protected routes enforce access through guard, which runs before any data fetcher can execute.
Setup
Register your application in Auth0 and note your credentials. Store them in environment variables — never hardcode them in specs.
# .env
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_CLIENT_ID=your_client_id
AUTH0_CLIENT_SECRET=your_client_secret
AUTH0_CALLBACK_URL=http://localhost:3000/auth/callback
Login route
The login route is a raw response spec that redirects the browser to Auth0's authorization endpoint.
// src/pages/auth/login.js
const {
AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CALLBACK_URL
} = process.env
export default {
route: '/auth/login',
contentType: 'text/html',
render: (ctx) => {
const params = new URLSearchParams({
response_type: 'code',
client_id: AUTH0_CLIENT_ID,
redirect_uri: AUTH0_CALLBACK_URL,
scope: 'openid profile email',
state: crypto.randomUUID(),
})
ctx.setHeader('Location', `https://${AUTH0_DOMAIN}/authorize?${params}`)
return { redirect: `https://${AUTH0_DOMAIN}/authorize?${params}` }
},
}
Callback route
Auth0 redirects back to /auth/callback with a code query parameter. The server exchanges it for tokens, sets a session cookie, and redirects to the app.
// src/pages/auth/callback.js
const {
AUTH0_DOMAIN, AUTH0_CLIENT_ID,
AUTH0_CLIENT_SECRET, AUTH0_CALLBACK_URL
} = process.env
export default {
route: '/auth/callback',
contentType: 'text/html',
server: {
session: async (ctx) => {
const { code } = ctx.query
if (!code) return null
// Exchange auth code for tokens
const res = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: AUTH0_CLIENT_ID,
client_secret: AUTH0_CLIENT_SECRET,
redirect_uri: AUTH0_CALLBACK_URL,
code,
}),
})
if (!res.ok) return null
const { access_token, id_token } = await res.json()
// Set a secure session cookie with the access token
ctx.setCookie('session', access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax',
maxAge: 86400, // 24 hours
})
return access_token
},
},
render: (ctx, server) => {
if (!server.session) return { redirect: '/auth/login' }
return { redirect: '/' }
},
}
Logout route
Clear the session cookie and redirect to Auth0's logout endpoint to invalidate the session there too.
// src/pages/auth/logout.js
const { AUTH0_DOMAIN, AUTH0_CLIENT_ID } = process.env
export default {
route: '/auth/logout',
contentType: 'text/html',
render: (ctx) => {
// Expire the session cookie
ctx.setCookie('session', '', { maxAge: 0 })
const returnTo = encodeURIComponent('http://localhost:3000')
return { redirect: `https://${AUTH0_DOMAIN}/v2/logout?client_id=${AUTH0_CLIENT_ID}&returnTo=${returnTo}` }
},
}
Protecting routes
Use guard to verify the session token before any server data is fetched. For production, verify the JWT signature locally rather than calling Auth0 on every request.
// src/pages/dashboard.js
export default {
route: '/dashboard',
guard: async (ctx) => {
if (!ctx.cookies.session) return { redirect: '/auth/login' }
// Optional: verify JWT signature for production
// const user = await verifyJwt(ctx.cookies.session)
// if (!user) return { redirect: '/auth/login' }
},
server: {
profile: async (ctx) => fetchUserProfile(ctx.cookies.session),
},
state: {},
view: (state, server) => `
<main id="main-content">
<h1>Welcome, ${server.profile.name}</h1>
</main>
`,
}
ctx reference
| Method | Description |
|---|---|
ctx.cookies.session | Read the session cookie set during OAuth callback |
ctx.setCookie(name, value, opts) | Set a response cookie — used in callback and logout routes |
ctx.setHeader(name, value) | Set an arbitrary response header |
| setCookie option | Type | Description |
|---|---|---|
httpOnly | boolean | Prevents JS access — always use for session cookies |
secure | boolean | HTTPS only — set true in production |
sameSite | "Lax" | "Strict" | "None" | Lax works for most OAuth flows |
maxAge | number | Lifetime in seconds — omit for session cookie |
path | string | Defaults to / |
domain | string | Scope to a domain — omit for current host |