@devcoffee/nuxt-core
@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 |
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: 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.