JWT Auth With Nitro

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.

5 min read Level 3/5 #nuxt#auth#jwt
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-utils ships sessions, OAuth, and password hashing out of the box.
Database Access — Drizzle / Prisma →