npm.io
1.6.2 • Published 2d ago

@devcoffee/nuxt-core

Licence
MIT
Version
1.6.2
Deps
4
Size
171 kB
Vulns
0
Weekly
48

@devcoffee/nuxt-core

npm version npm downloads License Nuxt

Full OpenID Connect / OAuth 2.0 authorization code grant with PKCE for DevCoffee internal Nuxt 4 applications. Provides server-side session management via Nitro, client-side auth state via Vue composables, and universal route protection middleware.

Features

  • PKCE-enforced authorization code flow (S256, enabled by default)
  • HMAC-SHA256 signed session cookies when sessions.secret is configured
  • AES-256-GCM encrypted tokenSet storage with HKDF-derived key
  • 256-bit session ID entropy via crypto.randomBytes(32)
  • Open redirect protection on all post-auth redirects
  • Per-route protection via definePageMeta — no page-level boilerplate
  • Auto-imported composables: useAuthContext, useSessionContext, useLogger
  • Server-side session validation on every Nitro request
  • Token refresh mutex — no concurrent refresh races
  • User info caching with configurable TTL
  • Nuxt DevTools integration (session inspector)

Requirements

  • Nuxt: ^4.0.0 — Nuxt 3 is not supported
  • Node.js: LTS (18+)
  • OIDC provider: Any provider with a .well-known/openid-configuration endpoint

Installation

npm install @devcoffee/nuxt-core

Register the module in nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['@devcoffee/nuxt-core'],
})

Quick Setup

1. Install and register the module (see Installation above).

2. Add the minimum config to nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['@devcoffee/nuxt-core'],
  nuxtCore: {
    authts: {
      openid: {
        wellKnownUrl: process.env.OIDC_WELL_KNOWN_URL!,
        clientId: process.env.OIDC_CLIENT_ID!,
        clientSecret: process.env.OIDC_CLIENT_SECRET!,
        redirectUri: '/authorize',
        scopes: ['openid', 'profile', 'email'],
      },
      sessions: {
        secret: process.env.SESSION_SECRET!,
      },
    },
  },
})

3. Create the server handlerrequired. Auth endpoints return 404 without this file.

Create server/api/_auth/[...].ts:

export default NuxtAuthtsHandler({
  userInfo: async (user, { openidUser }) => ({
    ...user,
    id: openidUser?.sub ?? user.id,
    email: openidUser?.email ?? user.email,
    firstName: openidUser?.given_name ?? user.firstName,
    lastName: openidUser?.family_name ?? user.lastName,
  }),
})

The OIDC callback page at openid.redirectUri (default /authorize) is auto-registered by the module. Do NOT create it manually.

4. Set environment variables:

OIDC_WELL_KNOWN_URL=https://your-provider/.well-known/openid-configuration
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
SESSION_SECRET=your-32-char-minimum-random-secret

Configuration Reference

All options are nested under nuxtCore in nuxt.config.ts.

authts.openid options
Option Type Default Description
wellKnownUrl string '' (required) OIDC provider discovery URL (/.well-known/openid-configuration endpoint)
clientId string '' (required) OAuth 2.0 client ID registered with the OIDC provider
clientSecret string '' (required) OAuth 2.0 client secret
redirectUri string '/authorize' OIDC callback path. Must match the redirect URI registered with your provider. The module auto-registers this page.
scopes string[] [] OAuth scopes to request. Include 'openid' at minimum.
usePkce boolean true Enable PKCE (S256). Strongly recommended — disabling reduces security.
codeChallengeMethod string 'S256' PKCE code challenge method
autoFetchUser boolean true Fetch user info from the OIDC userinfo endpoint on GET_SESSION
autoFetchUserTtl number 300 Userinfo cache TTL in seconds. Prevents redundant OIDC provider calls.
fetchUserOnLogin boolean true Fetch user info immediately after the token exchange
tokenRefreshBufferMs number 60000 Refresh tokens this many ms before expiry
cache.prefix string 'oidc-server-meta' Nitro cache key prefix for OIDC server metadata
cache.expires number 86400 OIDC metadata cache TTL in seconds (24 hours)
authts.sessions options
Option Type Default Description
secret string '' Secret for HMAC-SHA256 cookie signing and AES-256-GCM tokenSet encryption. Must be set in production (see Security). Empty string disables signing and encryption.
expiresIn number 518400 Session lifetime in seconds (default 6 days)
names.sessionId string 'auths.ssid' Session ID cookie name
names.state string 'auths.state' OAuth state cookie name
names.redirectUrl string 'auths.redirect' Redirect URL cookie name
names.pkce string 'auths.pkce' PKCE verifier cookie name
storage.name string 'sessions' Nitro storage mount name
storage.prefix string 'session' Nitro storage key prefix
cookieOpts.path string '/' Cookie path
cookieOpts.sameSite string 'lax' Cookie SameSite attribute
cookieOpts.httpOnly boolean true Cookie HttpOnly flag
cookieOpts.secure boolean true in production Cookie Secure flag
authts.auth options
Option Type Default Description
loginUri string '/login' Path middleware redirects unauthenticated users to
defaultLoginRedirectUri string '/' Default post-login redirect when no intended destination is recorded
defaultLogoutRedirectUri string '/login' Post-logout redirect
ignoreRegexPatterns RegExp[] [] Routes matching these patterns are excluded from middleware in all environments
ignoreRegexPatternsDev RegExp[] [] Routes excluded from middleware in development only
appendIgnoreRegexPatterns RegExp[] [] Additional routes appended to ignoreRegexPatterns, useful for downstream modules
appendIgnoreRegexPatternsDev RegExp[] [] Additional routes appended to ignoreRegexPatternsDev, useful for downstream modules
serverBypassRules { pattern: string | RegExp, mode: 'hard' | 'soft' | 'none' }[] built-ins Server auth plugin bypass rules; last matching rule wins. Patterns are normalized to serializable regex source strings
serverBypassRulesDev { pattern: string | RegExp, mode: 'hard' | 'soft' | 'none' }[] [] Development-only server auth plugin bypass rules
appendServerBypassRules { pattern: string | RegExp, mode: 'hard' | 'soft' | 'none' }[] [] Additional server bypass rules appended after serverBypassRules
appendServerBypassRulesDev { pattern: string | RegExp, mode: 'hard' | 'soft' | 'none' }[] [] Additional development-only server bypass rules
logging options
Option Type Default Description
logging.server.level number 2 Server log level (0=silent, 1=fatal, 2=error, 3=warn, 4=info/debug)
logging.server.tag string 'server' Server log tag
logging.ssr.tag string 'app-ssr' SSR log tag
logging.client.tag string 'app-client' Client log tag

Composables

All composables are auto-imported. No import statement needed.

useAuthContext(initiator?)

Reactive authentication state and actions.

const { login, logout, authorize, isAuthenticated, user, session, processing, sanitizeError } = useAuthContext()

// login — redirects to the OIDC provider's authorization endpoint
await login('/dashboard')

// logout — revokes tokens and redirects to defaultLogoutRedirectUri
await logout()

// authorize — exchanges the authorization code for tokens (called on the callback page)
await authorize(new URLSearchParams(window.location.search))

// isAuthenticated — ComputedRef<boolean>, true when the user has a valid session
console.log(isAuthenticated.value) // true | false

// user — ComputedRef<AuthorizedUser>, the current authenticated user
console.log(user.value.email)

// processing — Ref<boolean>, true while an auth request is in flight
console.log(processing.value) // true during login/logout/authorize

// sanitizeError — normalizes any error to H3Error
const h3err = sanitizeError(unknownError)
useSessionContext()

Low-level session accessor. Prefer useAuthContext for most use cases.

const { getValue, refetch } = useSessionContext()

// getValue — returns the current NuxtSessionContext
const session = getValue()

// refetch — triggers a session re-fetch from the server
await refetch()
useLogger(opts?)

Create a tagged logger instance.

const logger = useLogger({ tag: 'my-feature', level: 4 })

logger.debug('Debug message')
logger.warn('Warning message')
logger.error('Error message', error)

Log levels: 0 silent, 1 fatal, 2 error (default), 3 warn, 4 info/debug

Server Handlers

Server handlers are auto-imported into Nitro routes. No import needed.

NuxtAuthtsHandler(options?)

Main auth handler. Required — create this file or auth endpoints return 404.

// server/api/_auth/[...].ts
export default NuxtAuthtsHandler({
  userInfo: async (user, { openidUser }) => ({
    ...user,
    id: openidUser?.sub ?? user.id,
    email: openidUser?.email ?? user.email,
    firstName: openidUser?.given_name ?? user.firstName,
    lastName: openidUser?.family_name ?? user.lastName,
  }),
})

The userInfo callback maps OIDC claims to AuthorizedUser. It is called after the token exchange (when fetchUserOnLogin is true) and on every GET_SESSION request when autoFetchUser is true (results are cached for autoFetchUserTtl seconds).

NuxtForwardRequestHandler(opts)

Authenticated reverse proxy — forwards requests to a backend service with the session access token attached.

// server/api/backend/[...].ts
export default NuxtForwardRequestHandler({
  targetBaseUrl: process.env.BACKEND_API_URL,
  proxyPrefix: '/api/backend',
})

Route Protection

The module registers a global Nuxt middleware that runs on every navigation. Control access per page with definePageMeta.

Require authentication
<!-- pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  authts: { required: true },
})

const { user, isAuthenticated, logout } = useAuthContext()
</script>

<template>
  <div>
    <p>Welcome, {{ user.firstName }}</p>
    <button @click="logout()">Sign out</button>
  </div>
</template>

Unauthenticated users are redirected to auth.loginUri (default /login). The intended URL is preserved as the post-login redirect.

Restrict to unauthenticated users (login page)
<!-- pages/login.vue -->
<script setup lang="ts">
definePageMeta({
  authts: { unauthenticatedOnly: true },
})

const { login } = useAuthContext()
</script>

<template>
  <button @click="login('/dashboard')">Sign in</button>
</template>

Authenticated users navigating to this page receive a 404 from middleware.

Warning: Do not set both required: true and unauthenticatedOnly: true on the same page. This combination causes a middleware error.

Note: roles is a reserved field in AuthtsMiddlewareMeta and is not currently enforced.

Nuxt Hooks

// plugins/my-app.ts
export default defineNuxtPlugin((_nuxtApp) => {
  // Fires after every successful authorization code exchange
  _nuxtApp.hook('user:loggedIn', async () => {
    console.log('User logged in — session is now populated')
  })

  // Fires after successful logout
  _nuxtApp.hook('user:loggedOut', async () => {
    console.log('User logged out — session has been cleared')
  })

  // Trigger a session re-fetch manually (e.g. after a server-side state change)
  // _nuxtApp.callHook('session:fetch', 'my-initiator')

  // Fires after every session fetch — receives the latest session data
  _nuxtApp.hook('session:changed', async (session) => {
    console.log('Session updated:', session.isAuthenticated)
  })
})

TypeScript

import type {
  AuthorizedUser,
  AuthtsMiddlewareMeta,
  NuxtSessionContext,
  SessionContext,
} from '@devcoffee/nuxt-core'
Custom session data

SessionContext and NuxtSessionContext accept a generic TData parameter for type-safe custom session data.

import type { SessionContext } from '@devcoffee/nuxt-core'

type MyAppData = {
  preferences: { theme: string }
  roles: string[]
}

// In a server handler or Nitro plugin:
const session: SessionContext<MyAppData> = await getSession(sessionId, opts)

// Fully typed — no casting required
console.log(session.data.preferences.theme) // string
console.log(session.data.roles)             // string[]

Security

Production checklist

1. Set sessions.secret — most important step.

Without it, session ID cookies are unsigned and tokenSet storage is unencrypted. An attacker who gains read access to Nitro storage can extract access tokens.

Set a 32+ character random string:

SESSION_SECRET=your-32-char-minimum-random-secret

When sessions.secret is configured:

  • HMAC-SHA256 signs the session ID cookie — tampering is detected with constant-time comparison
  • AES-256-GCM encrypts the tokenSet in Nitro storage — keys are derived via HKDF-SHA256 with domain separation

2. PKCE is enabled by defaultusePkce: true with S256. Do not disable it.

3. Open redirect protectionisSameOrigin() validates all post-auth redirects. External URLs are rejected with HTTP 400.

4. Session ID entropycrypto.randomBytes(32), 256-bit. UUID v4 is not used.

5. HttpOnly cookieshttpOnly: true and secure: true in production by default.

Changelog

See CHANGELOG.md for the full release history.

Contributing

See GUIDELINE.md for contribution guidelines, local development setup, and release instructions.

Keywords