Sign on Login, Verify in Middleware
JWT Auth With Nitro
Build basic auth from scratch — a login route signs a JWT, stores it as an httpOnly cookie, and middleware verifies it on every request.
What you'll learn
- Sign a JWT in a login route using the jose library
- Store the token in an httpOnly secure cookie
- Verify the token in server middleware and attach the user to event.context
Rolling your own auth is a useful exercise to understand the pieces. For production, prefer a
battle-tested module like nuxt-auth-utils or @sidebase/nuxt-auth — but the moving parts look
the same.
Setup
npm i jose Store the secret in runtime config:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
jwtSecret: '', // set via NUXT_JWT_SECRET
},
}) Shared Helpers
// server/utils/jwt.ts
import * as jose from 'jose'
const getSecret = () =>
new TextEncoder().encode(useRuntimeConfig().jwtSecret)
export async function signToken(payload: { sub: string }) {
return await new jose.SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(getSecret())
}
export async function verifyToken(token: string) {
const { payload } = await jose.jwtVerify(token, getSecret())
return payload as { sub: string }
} Login Route
// server/api/login.post.ts
export default defineEventHandler(async (event) => {
const { email, password } = await readBody<{ email: string; password: string }>(event)
const user = await authenticateUser(email, password)
if (!user) {
throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' })
}
const token = await signToken({ sub: user.id })
setCookie(event, 'token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 60 * 60,
})
return { id: user.id, email: user.email }
}) Auth Middleware
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'token')
if (!token) return
try {
const payload = await verifyToken(token)
event.context.user = { id: payload.sub }
} catch {
deleteCookie(event, 'token')
}
}) Routes that require auth just check event.context.user:
// server/api/me.get.ts
export default defineEventHandler((event) => {
if (!event.context.user) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
return event.context.user
}) Production Considerations
- Rotate the JWT secret regularly and revoke active sessions on rotation.
- Use refresh tokens for long sessions instead of long-lived access tokens.
- Consider a real module:
nuxt-auth-utilsships sessions, OAuth, and password hashing out of the box.