@icecat-studio/nuxt-auth
v0.1.0
Published
Modern authentication module for Nuxt 3+ with token-based auth, auto-refresh, SSR support, and smart tab coordination
Maintainers
Readme
@icecat-studio/nuxt-auth
Modern authentication module for Nuxt 3+ with token-based auth, auto-refresh, SSR support, and smart tab coordination.
Features
- 🔐 Token-based Authentication — JWT/Bearer token support out of the box
- 🔄 Auto-refresh Tokens — Automatic token renewal with configurable intervals
- 🖥️ SSR Ready — Full server-side rendering support with proper hydration
- 🎯 Smart Tab Coordination — Prevents multiple tabs from refreshing simultaneously
- ⏸️ Pause on Inactive — Automatically pauses refresh when tab is hidden
- 🛡️ Auth Fetch — Composable with automatic auth headers and 401 retry
- 🎨 Flexible Token Management — Client-managed, server-managed (httpOnly), or no refresh
- 📦 Type-safe — Full TypeScript support with user type augmentation
- 🚀 Composable API — Simple
useAuth()anduseAuthFetch()composables - 🔧 Route Middleware — Built-in auth middleware for route protection
Quick Setup
Install the module:
npm install @icecat-studio/nuxt-authAdd it to nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@icecat-studio/nuxt-auth'],
auth: {
baseUrl: '/api/auth',
endpoints: {
login: { path: '/login', method: 'post' },
logout: { path: '/logout', method: 'post' },
refresh: { path: '/refresh', method: 'post' },
user: { path: '/user', method: 'get' },
},
},
})That's it! You can now use @icecat-studio/nuxt-auth in your Nuxt app ✨
Usage
Login
<script setup lang="ts">
const auth = useAuth()
const login = async () => {
try {
await auth.login({
email: '[email protected]',
password: 'password',
})
// Redirects to home page automatically
}
catch (error) {
console.error('Login failed:', error)
}
}
</script>You can control redirect behavior per call:
// Disable redirect
await auth.login(credentials, { redirect: false })
// Custom redirect path
await auth.login(credentials, { redirect: '/dashboard' })
// External redirect
await auth.login(credentials, { redirect: { url: 'https://app.example.com', external: true } })Registration
<script setup lang="ts">
const auth = useAuth()
const register = async () => {
try {
await auth.register({
name: 'John Doe',
email: '[email protected]',
password: 'password',
})
// If the API returns tokens — logs in automatically
// Otherwise — redirects to login page
}
catch (error) {
console.error('Registration failed:', error)
}
}
</script>Protect Routes
<script setup lang="ts">
definePageMeta({
auth: true, // Requires authentication
})
</script>For guest-only pages (login, register):
<script setup lang="ts">
definePageMeta({
auth: 'guest', // Redirects authenticated users away
})
</script>Public pages (opt out of global middleware):
<script setup lang="ts">
definePageMeta({
auth: false, // Accessible to everyone
})
</script>Access User Data
<script setup lang="ts">
const auth = useAuth()
</script>
<template>
<div v-if="auth.loggedIn.value">
<p>Welcome, {{ auth.user.value?.name }}!</p>
<button @click="auth.logout()">Logout</button>
</div>
</template>Authenticated HTTP Requests
Use useAuthFetch() for requests that need authentication. It automatically injects auth headers and handles 401 responses:
<script setup lang="ts">
const authFetch = useAuthFetch()
const { data } = await useAsyncData(() => authFetch('/api/orders'))
</script>useAuthFetch provides:
- Automatic
Authorizationheader injection - Proactive token refresh if the access token is missing but refresh is available
- Automatic retry on 401 after a successful token refresh (client-side)
- Cookie forwarding during SSR for server-managed tokens
Authentication Modes
The module supports three token management strategies:
Client-Managed Refresh Token
The refresh token is stored in a client-accessible cookie and sent in the request body during refresh.
export default defineNuxtConfig({
auth: {
refreshToken: {
serverManaged: false, // default
property: 'refreshToken',
bodyProperty: 'refreshToken',
},
},
})Server-Managed Refresh Token (httpOnly)
The refresh token is managed entirely by the server via an httpOnly cookie. It is never accessible to JavaScript — the browser sends it automatically.
export default defineNuxtConfig({
auth: {
refreshToken: {
serverManaged: true,
},
},
})No Refresh (Access Token Only)
Disable the refresh endpoint for simple access-token-only flows. The session ends when the token expires.
export default defineNuxtConfig({
auth: {
endpoints: {
refresh: false,
},
},
})Configuration
Endpoints
Each endpoint can be configured or disabled individually:
export default defineNuxtConfig({
auth: {
baseUrl: '/api/auth',
endpoints: {
login: { path: '/login', method: 'post' },
logout: { path: '/logout', method: 'post' }, // set to `false` to disable
register: { path: '/register', method: 'post' }, // set to `false` to disable
refresh: { path: '/refresh', method: 'post' }, // set to `false` to disable
user: { path: '/user', method: 'get' },
},
},
})You can also pass additional fetch options per endpoint:
endpoints: {
login: {
path: '/login',
method: 'post',
fetchOptions: { credentials: 'include' },
},
}Access Token
export default defineNuxtConfig({
auth: {
accessToken: {
property: 'accessToken', // Property in API response
cookieName: 'auth.access_token',
type: 'Bearer', // Token type prefix
headerName: 'Authorization', // Header name
maxAge: 900, // 15 minutes (in seconds)
httpOnly: false,
secure: true,
sameSite: 'lax',
path: '/',
},
},
})Refresh Token
export default defineNuxtConfig({
auth: {
refreshToken: {
property: 'refreshToken', // Property in API response
cookieName: 'auth.refresh_token',
serverManaged: false, // true = httpOnly cookie managed by server
bodyProperty: 'refreshToken', // Key name in refresh request body
maxAge: 604800, // 7 days (in seconds)
httpOnly: false,
secure: true,
sameSite: 'lax',
path: '/',
},
},
})Auto-Refresh
export default defineNuxtConfig({
auth: {
autoRefresh: {
enabled: true,
interval: 840, // 14 minutes (in seconds)
pauseOnInactive: true, // Pause when tab is hidden
enableTabCoordination: true, // Coordinate between tabs
coordinationCookieName: 'auth.last_refresh',
coordinationThreshold: 5, // 5 seconds — skip if another tab refreshed within this time
},
},
})Redirects
export default defineNuxtConfig({
auth: {
redirect: {
login: '/login', // Where to redirect unauthenticated users
logout: '/', // Where to redirect after logout
home: '/', // Where to redirect after login
},
},
})Redirects also support external URLs:
redirect: {
home: { url: 'https://app.example.com/dashboard', external: true },
}User
export default defineNuxtConfig({
auth: {
user: {
property: undefined, // Nested property path (e.g., 'data.user'), undefined = entire response
autoFetch: true, // Automatically fetch user after login/refresh
},
},
})Global Middleware
Enable authentication check on all routes by default:
export default defineNuxtConfig({
auth: {
globalMiddleware: true,
},
})Then opt out specific routes with auth: false in definePageMeta.
API Reference
useAuth<T>()
The main composable for authentication. Returns a singleton instance.
State
| Property | Type | Description |
|---|---|---|
| user | Ref<T \| null> | Current user data (reactive) |
| status | Ref<AuthStatus> | Auth status (see below) |
| loggedIn | ComputedRef<boolean> | Whether the user is logged in |
| accessToken | Ref<string \| null> | Current access token |
| refreshToken | Ref<string \| null> | Current refresh token (null if server-managed) |
| canRefresh | ComputedRef<boolean> | Whether token refresh is available |
Auth status values:
| Status | Description |
|---|---|
| idle | Initial state, no auth activity |
| loading | Login or registration in progress |
| refreshing | Token refresh in progress |
| authenticated | User is authenticated |
| unauthenticated | No valid session |
loggedIn logic:
- With refresh endpoint:
truewhen status isauthenticatedorrefreshing(session is alive until refresh explicitly fails) - Without refresh endpoint:
truewhen status isauthenticatedand access token exists
Methods
| Method | Signature | Description |
|---|---|---|
| login | (credentials: Record<string, any>, options?: LoginOptions) => Promise<void> | Login with credentials |
| logout | () => Promise<void> | Logout and clear session |
| register | (data: Record<string, any>) => Promise<void> | Register a new user |
| refresh | () => Promise<void> | Manually refresh tokens |
| fetchUser | () => Promise<void> | Fetch user data from the user endpoint |
| getAuthHeaders | () => Record<string, string> | Get authorization headers for manual requests |
useAuthFetch()
Returns an enhanced $fetch function with automatic auth handling:
const authFetch = useAuthFetch()
// Typed response
const user = await authFetch<User>('/api/me')
// With options
const data = await authFetch('/api/data', { method: 'POST', body: { key: 'value' } })Behavior:
- Injects
Authorizationheader with the current access token - If no access token but refresh is available — refreshes first, then makes the request
- On 401 response (client-side only) — refreshes the token and retries the request once
- During SSR — forwards incoming request cookies for server-managed tokens
Advanced Usage
Custom API Calls with Auth Headers
If you need auth headers without using useAuthFetch:
const auth = useAuth()
const data = await $fetch('/api/protected', {
headers: auth.getAuthHeaders(),
})Manual Token Refresh
const auth = useAuth()
try {
await auth.refresh()
console.log('Token refreshed successfully')
}
catch (error) {
console.error('Refresh failed:', error)
}Controlling the Refresh Manager
The auto-refresh scheduler is exposed via useNuxtApp().$refreshManager:
const { $refreshManager } = useNuxtApp()
$refreshManager?.stop() // Stop auto-refresh
$refreshManager?.start() // Start auto-refresh
$refreshManager?.refresh() // Trigger immediate refreshTypeScript: Augmenting the User Type
Define your user interface globally:
// types/auth.d.ts
declare module '@icecat-studio/nuxt-auth' {
interface User {
id: number
email: string
name: string
avatar?: string
}
}Then useAuth() will infer the correct type:
const auth = useAuth()
auth.user.value?.name // string
auth.user.value?.id // numberOr pass the type parameter directly:
interface MyUser {
id: number
name: string
}
const auth = useAuth<MyUser>()How It Works
Session Initialization
- On app start, checks for existing tokens in cookies
- SSR: Attempts token refresh on the server; stores the result for the client
- Client hydration: If the server already refreshed, skips duplicate refresh and fetches user data
- Client-only navigation: Refreshes tokens and restores session normally
Auto-Refresh
- Schedules token refresh at the configured interval (default: 14 minutes)
- Pauses when the tab is hidden (if
pauseOnInactiveenabled) - Resumes with an immediate refresh when the tab becomes visible
- On network error — reschedules with a doubled interval
- On server rejection (4xx) — stops auto-refresh and clears session
Tab Coordination
- Uses a shared cookie to track the last refresh timestamp
- Before refreshing, checks if another tab refreshed recently (within
coordinationThreshold) - Skips refresh if within the threshold, reschedules normal interval
- Updates the timestamp after a successful refresh
Error Handling
| Scenario | Behavior |
|---|---|
| Server rejection (4xx) | Clears tokens, sets status to unauthenticated, stops auto-refresh |
| Network error | Keeps tokens intact, reverts status, reschedules with doubled interval |
| 401 in useAuthFetch | Refreshes token and retries request once (client-only) |
| Middleware: protected page, no session | Attempts refresh; redirects to login on failure |
Default Configuration
Full configuration with all default values:
export default defineNuxtConfig({
auth: {
baseUrl: '/api/auth',
endpoints: {
login: { path: '/login', method: 'post' },
logout: { path: '/logout', method: 'post' },
register: { path: '/register', method: 'post' },
refresh: { path: '/refresh', method: 'post' },
user: { path: '/user', method: 'get' },
},
accessToken: {
property: 'accessToken',
cookieName: 'auth.access_token',
httpOnly: false,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 900, // 15 minutes
type: 'Bearer',
headerName: 'Authorization',
},
refreshToken: {
property: 'refreshToken',
cookieName: 'auth.refresh_token',
httpOnly: false,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 604800, // 7 days
serverManaged: false,
bodyProperty: 'refreshToken',
},
autoRefresh: {
enabled: true,
interval: 840, // 14 minutes
pauseOnInactive: true,
enableTabCoordination: true,
coordinationCookieName: 'auth.last_refresh',
coordinationThreshold: 5, // 5 seconds
},
user: {
property: undefined,
autoFetch: true,
},
redirect: {
login: '/login',
logout: '/',
home: '/',
},
globalMiddleware: false,
},
})Contribution
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with client-managed mode
npm run dev:client
# Develop with server-managed mode
npm run dev:server
# Develop without token refresh
npm run dev:no-refresh
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Run type checking
npm run test:types
# Release new version
npm run release