@devcoffee/nuxt-core
| Install | |
|---|---|
npm install @devcoffee/nuxt-core |
|
| Latest Version: | 2.0.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 19, 2026 |
| Links: | npm |
@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.
- Release Notes
- Migrating from v1 to v2
- Contributor Guide
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 — atomic Redis lock with
distributedLock: true, so concurrent requests share one refresh - 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-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 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-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 |
distributedLock |
boolean |
false |
Use an atomic Redis SET NX EX lock for token refresh. Requires the Nitro cache storage mount to be Redis-backed; fails closed (throws) otherwise. When false, an optimistic lock is used that does NOT guarantee mutual exclusion. |
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 |
ignorePatterns |
string[] |
[] |
Glob patterns (picomatch: * within a segment, ** across segments, dot segments included), e.g. /public/**. Request paths matching any pattern are ignored by auth in both the route middleware (skips redirects and meta checks) and the Nitro plugin (read-only session: skips validation, token refresh and userInfo, still writes the session cookie). Matching is case-insensitive (the path is lowercased before testing). Server-only built-ins (/__devcoffee_core_session_devtools__, /_i18n/**, /_nuxt/**, /__nuxt*, /__nuxt*/**) are always applied first and cannot be replaced. Non-string entries are dropped; patterns are exposed in the public runtime config |
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-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 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.
Migrating from v1 to v2
v2 replaces the route ignore / server bypass options with a single glob-based ignorePatterns option, makes the distributed refresh lock fail closed, and removes the v1 compatibility import paths. Composables, server handlers (NuxtAuthtsHandler, NuxtForwardRequestHandler), route meta, hooks and the openid / sessions / logging options are unchanged.
1. Replace ignore and bypass options with auth.ignorePatterns
These authts.auth options are removed:
| Removed (v1) | v2 replacement |
|---|---|
ignoreRegexPatterns: RegExp[] |
ignorePatterns: string[] (glob) |
serverBypassRules: { pattern, mode }[] |
ignorePatterns: string[] (glob) — modes are gone, see below |
ignoreRegexPatternsDev, serverBypassRulesDev |
none — add the patterns conditionally in nuxt.config.ts |
appendIgnoreRegexPatterns(Dev), appendServerBypassRules(Dev) |
none — list every pattern in the app's ignorePatterns |
ignorePatterns applies to both layers that previously needed two options: the route middleware (no login redirect, no meta checks) and the Nitro request plugin. Entries are picomatch globs, not regular expressions; RegExp objects are no longer accepted and non-string entries are dropped.
// v1
nuxtCore: {
authts: {
auth: {
ignoreRegexPatterns: [/^\/\.well-known\/appspecific\/com\.chrome\.devtools\.json$/],
serverBypassRules: [
{ mode: 'hard', pattern: /^\/\.well-known\/appspecific\/com\.chrome\.devtools\.json$/ },
],
},
},
}
// v2
nuxtCore: {
authts: {
auth: {
ignorePatterns: ['/.well-known/appspecific/com.chrome.devtools.json'],
},
},
}
Converting regular expressions to globs:
| v1 regex | v2 glob | Notes |
|---|---|---|
/^\/public$/ |
'/public' |
exact path |
/^\/public\// or /^\/public/ |
'/public/**' |
** also matches /public itself; add '/public*' to also match siblings such as /public-docs |
/^\/api\/[^/]+$/ |
'/api/*' |
* stays within one path segment |
/^\/docs\/.*\.pdf$/ |
'/docs/**/*.pdf' |
|
/^\/\.well-known\// |
'/.well-known/**' |
dot segments match without extra options |
/^\/Admin/i |
'/admin/**' |
matching is always case-insensitive (the path is lowercased before testing) |
Development-only patterns now live in your config:
const devIgnorePatterns = process.env.NODE_ENV === 'production' ? [] : ['/__dev-tools/**']
export default defineNuxtConfig({
nuxtCore: {
authts: {
auth: { ignorePatterns: ['/public/**', ...devIgnorePatterns] },
},
},
})
The built-in server patterns (/__devcoffee_core_session_devtools__, /_i18n/**, /_nuxt/**, /__nuxt*, /__nuxt*/**) are always applied by the Nitro plugin and can no longer be removed or overridden, either from config or with a runtime environment override. Remove any copies of them from your config.
2. Behavior change for ignored requests
The hard / soft / none bypass modes no longer exist. A request whose path matches ignorePatterns (or a built-in pattern) now:
- reads an existing, unexpired stored session read-only into
event.context.session, or gets an in-memory anonymous session when there is none (it is not persisted); - skips session validation and expiry extension, token refresh and userInfo fetching;
- still writes the session cookie for a stored session (v1
hardrules skipped it); no cookie is written for the in-memory anonymous session.
Matching is path-only. The v1 x-nuxt-i18n: internal request-header bypass is removed: the @nuxtjs/i18n internal messages fetch is covered by the built-in /_i18n/** pattern. If you changed the i18n serverRoutePrefix, add that prefix to ignorePatterns.
Because the session is not refreshed on these paths, its access token may already be expired. Do not call token-protected upstream APIs (for example through NuxtForwardRequestHandler) from ignored paths. For a public page that still needs a fresh session, use definePageMeta({ authts: { required: false } }) instead of an ignore pattern.
3. distributedLock: true now fails closed
With openid.distributedLock: true, the Nitro cache storage mount must use the Redis driver. In v1 a missing or failing Redis client silently fell back to the optimistic lock; v2 throws instead, and the request ends with an auth failure. The lock key now matches the key the Redis driver uses for storage reads, and the refresh grant has a 10 s timeout with a 15 s lock TTL.
export default defineNuxtConfig({
nitro: {
storage: {
cache: { driver: 'redis' /* host, port, password, ... */ },
},
},
nuxtCore: {
authts: { openid: { distributedLock: true } },
},
})
distributedLock: false (the default) keeps the optimistic lock, which does not guarantee mutual exclusion.
4. Removed #devcoffee-core/* import paths
The v1 re-export shims are removed. Only code that imported module internals is affected; the auto-imported composables and server handlers are not.
| Removed path | Use instead |
|---|---|
#devcoffee-core/utils |
#devcoffee-core/shared/utils |
#devcoffee-core/server/adapters/utils |
#devcoffee-core/shared/utils |
#devcoffee-core/server/core |
named imports from #devcoffee-core/server/lib/session/*, #devcoffee-core/server/lib/oidc/*, #devcoffee-core/server/lib/url |
#devcoffee-core/server/core/helpers |
#devcoffee-core/server/lib/session/{store,lifecycle,cookie}, #devcoffee-core/server/lib/oidc/{discovery,authorize,token,refresh,userinfo,revoke}, #devcoffee-core/server/lib/url |
#devcoffee-core/server/core/crypto |
#devcoffee-core/server/lib/crypto |
#devcoffee-core/server/core/mutex |
#devcoffee-core/server/lib/mutex (tryAcquireLock / releaseLock are replaced by withLock) |
#devcoffee-core/server/core/authRefreshError |
#devcoffee-core/server/lib/errors |
#devcoffee-core/server/core/nuxtAuthtsHandler |
#devcoffee-core/server/handlers/auth |
#devcoffee-core/server/core/nuxtForwardHandler |
#devcoffee-core/server/handlers/forward |
5. Type and runtime config changes
AuthRequestBypassModeandAuthRequestBypassRuleare no longer exported. Ignore patterns are plainstring[].- The public runtime config (
useRuntimeConfig().public.nuxtCore.authts) exposesignorePatterns: string[];ignoreRegexPatternsandignoreRegexPatternsDevare gone. - Newly exported types:
AuthOptions,AuthRequestFailure,AuthStatus,NuxtCorePublicRuntimeConfig,OidcUserInfo,OpenIdOptions,SessionsOptions. ModulePublicRuntimeConfigis still exported but deprecated; useNuxtCorePublicRuntimeConfig.
6. New dependency
picomatch is now a runtime dependency of the module and is installed automatically.
Upgrade checklist
- Replace
ignoreRegexPatterns,serverBypassRulesand theirDev/append*variants with oneignorePatternslist of globs. - Remove copies of the built-in patterns from your config.
- Check that no ignored path relies on a refreshed access token.
- With
distributedLock: true, confirm the Nitrocachemount uses the Redis driver. - Update any
#devcoffee-core/*imports to the paths above. - Replace
ModulePublicRuntimeConfigwithNuxtCorePublicRuntimeConfig.
Changelog
See CHANGELOG.md for the full release history.
Contributing
See GUIDELINE.md for contribution guidelines, local development setup, and release instructions.
Related Packages
Nuxt module that signs your app in with Authup and guards its routes by login st...
A comprehensive authentication integration for Nuxt 4 applications with TypeScri...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 2.0.0 | ^3.0 | MIT | |
| 1.9.2 | ^3.0 | MIT | |
| 1.9.1 | ^3.0 | MIT | |
| 1.9.0 | ^3.0 | MIT | |
| 1.8.0 | ^3.0 | MIT | |
| 1.7.1 | ^3.0 | MIT | |
| 1.7.0 | ^3.0 | MIT | |
| 1.6.2 | ^3.0 | MIT | |
| 1.6.1 | ^3.0 | MIT | |
| 1.6.0 | ^3.0 | MIT | |
| 1.5.1 | ^3.0 | MIT | |
| 1.5.0 | ^3.0 | MIT | |
| 1.4.3 | ^3.0 | MIT | |
| 1.4.2 | ^3.0 | MIT | |
| 1.4.1 | ^3.0 | MIT | |
| 1.4.0 | ^3.0 | MIT | |
| 1.3.0 | ^3.0 | MIT | |
| 1.2.5 | ^3.0 | MIT | |
| 1.2.4 | ^3.0 | MIT | |
| 1.2.3 | ^3.0 | MIT | |
| 1.2.0 | ^3.0 | MIT | |
| 1.1.1 | ^3.0 | MIT | |
| 1.1.0 | ^3.0 | MIT | |
| 1.0.2 | ^3.0 | MIT | |
| 1.0.1 | ^3.0 | MIT | |
| 1.0.0 | ^3.0 | MIT |