@devcoffee/nuxt-core
v1.2.5
Published
Nuxt 4 module providing OpenID Connect / OAuth 2.0 authorization code grant with PKCE, server-side session management via Nitro, client-side auth state composables, and universal route protection middleware.
Downloads
659
Maintainers
Readme
@devcoffee/nuxt-core
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.secretis 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-configurationendpoint
Installation
npm install @devcoffee/nuxt-coreRegister 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 handler — required. 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-secretConfiguration 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 |
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: trueandunauthenticatedOnly: trueon the same page. This combination causes a middleware error.
Note:
rolesis a reserved field inAuthtsMiddlewareMetaand 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-secretWhen 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 default — usePkce: true with S256. Do not disable it.
3. Open redirect protection — isSameOrigin() validates all post-auth redirects. External URLs are rejected with HTTP 400.
4. Session ID entropy — crypto.randomBytes(32), 256-bit. UUID v4 is not used.
5. HttpOnly cookies — httpOnly: 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.
