nuxt-x-auth-local
nuxt-x-auth-local
Self-hosted JWT authentication layer for Nuxt. Connects to your own backend API — any server that accepts email/password and returns a JWT works. Provides useXAuth with shared state via useState, automatic token refresh on 401, cookie-based token storage, 4 pre-built auth UI components, and a global route guard.
Installation
npm install @xenterprises/nuxt-x-auth-local
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-auth-local'],
runtimeConfig: {
public: {
localAuth: {
baseUrl: process.env.NUXT_PUBLIC_LOCAL_AUTH_BASE_URL || 'http://localhost:3001',
},
},
},
})
Configuration
runtimeConfig.public.localAuth
All endpoint paths are relative to baseUrl and have sensible defaults.
| Key | Default | Description |
|---|---|---|
baseUrl | "" | Backend API base URL (required). |
loginEndpoint | "/auth/login" | POST endpoint for email/password login. |
signupEndpoint | "/auth/signup" | POST endpoint for registration. |
logoutEndpoint | "/auth/logout" | POST endpoint for logout. |
refreshEndpoint | "/auth/refresh" | POST endpoint for token refresh. |
userEndpoint | "/auth/me" | GET endpoint to fetch the current user. |
forgotPasswordEndpoint | "/auth/forgot-password" | POST endpoint to request a reset email. |
resetPasswordEndpoint | "/auth/reset-password" | POST endpoint to apply a new password. |
changePasswordEndpoint | "/auth/change-password" | POST endpoint for authenticated password change. |
app.config.ts
export default defineAppConfig({
xAuth: {
tokens: {
accessCookie: 'x_auth_access', // cookie name for access token
refreshCookie: 'x_auth_refresh', // cookie name for refresh token
hasRefresh: true, // set false if your API has no refresh token
},
redirects: {
login: '/auth/login',
signup: '/auth/signup',
afterLogin: '/',
afterSignup: '/',
afterLogout: '/auth/login',
forgotPassword: '/auth/forgot-password',
},
features: {
forgotPassword: true,
signup: true,
},
ui: {
showLogo: true,
logoUrl: '',
brandName: 'My App',
tagline: '',
layout: 'centered', // 'centered' | 'split'
background: {
type: 'gradient', // 'gradient' | 'image' | 'solid'
imageUrl: '',
overlayOpacity: 50,
},
card: {
glass: false,
glassIntensity: 'medium', // 'subtle' | 'medium' | 'strong'
},
split: {
heroPosition: 'left', // 'left' | 'right'
heroImageUrl: '',
headline: '',
subheadline: '',
features: [],
},
},
},
})
useXAuth Composable
State is shared across components via Nuxt's useState — all instances of useXAuth on the same page share the same user and isLoading refs.
const {
// State
user, // Ref<AuthUser | null> — shared via useState
isLoading, // Ref<boolean> — shared via useState
isAuthenticated, // ComputedRef<boolean>
emailSent, // Ref<boolean> — true after forgotPassword
// Core
login, // (email, password) => Promise<AuthUser | null>
signup, // (email, password) => Promise<AuthUser | null>
logout, // () => Promise<true>
// Password management
forgotPassword, // (email) => Promise<boolean>
resetPassword, // (token, newPassword) => Promise<true | { error }>
changePassword, // (currentPassword, newPassword) => Promise<true | { error }>
// Token / session
refreshToken, // () => Promise<AuthTokens | null>
getCurrentUser, // () => Promise<AuthUser | null>
getToken, // () => string | null (synchronous)
getAuthHeaders, // () => Record<string, string> (synchronous)
resetState, // () => void — clears emailSent
config, // ComputedRef<AuthConfig>
} = useXAuth()
AuthUser shape
{
id: string
email: string
name: string
avatar?: string
emailVerified: boolean
metadata?: Record<string, any>
}
User data is normalized from your API response using fieldMapper — it tries common field name variants (id/sub/user_id, name/displayName/full_name, etc.) automatically.
Expected API response shapes
Login / Signup:
{ "accessToken": "eyJ...", "refreshToken": "eyJ..." }
Also accepts token as an alias for accessToken.
Current user (/auth/me):
{ "user": { "id": "1", "email": "user@example.com", "name": "Jane" } }
Or a flat object: { "id": "1", "email": "...", "name": "..." }.
Token refresh (/auth/refresh):
{ "accessToken": "eyJ...", "refreshToken": "eyJ..." }
Automatic token refresh
On any 401 response, the composable automatically calls refreshEndpoint with the stored refresh token and retries the original request once. If refresh fails, cookies are cleared. Disable this by setting tokens.hasRefresh: false.
Security
All redirect targets are validated to be relative paths (starting with /). Protocol-relative (//) and absolute URLs are silently replaced with / to prevent open redirect attacks.
Examples
// Login
const user = await login('user@example.com', 'password')
// Protect an API call
const headers = getAuthHeaders()
// { Authorization: 'Bearer eyJ...' }
// Change password (authenticated)
const result = await changePassword('oldPass', 'newPass')
if (result !== true) console.error(result.error)
// Reset password from email link
const result = await resetPassword(route.query.token, newPassword)
Pre-Built Pages
| Route | Description |
|---|---|
/auth/login | Login form |
/auth/signup | Registration form (if features.signup: true) |
/auth/forgot-password | Password reset request form |
/auth/reset-password | New password form (reads token from query string) |
/auth/logout | Calls logout() and redirects |
Components
All 4 components are auto-imported with the XAuth prefix.
| Component | Description |
|---|---|
<XAuthLogin /> | Email/password login form with forgot-password link |
<XAuthSignup /> | Registration form |
<XAuthForgotPassword /> | Password reset request; shows success state after submit |
<XAuthForm /> | Shared card/layout/branding shell used by the page components |
Global Route Middleware
auth.global.ts runs on every navigation:
| Route type | Behavior |
|---|---|
Guest-only (/auth/login, /auth/signup, /auth/forgot-password, /auth/reset-password) | Authenticated users → redirect to redirects.afterLogin |
Public (/auth/logout) | Always allowed |
| All other routes | Unauthenticated users → redirect to redirects.login |
Token Storage
Tokens are stored in cookies (names configured via tokens.accessCookie and tokens.refreshCookie). The auth-token plugin attaches the access token as a Bearer header on outgoing $fetch requests automatically.
Environment Variables
| Variable | Description |
|---|---|
NUXT_PUBLIC_LOCAL_AUTH_BASE_URL | Backend API base URL (required) |
NUXT_PUBLIC_LOCAL_AUTH_LOGIN_ENDPOINT | Override default /auth/login |
NUXT_PUBLIC_LOCAL_AUTH_SIGNUP_ENDPOINT | Override default /auth/signup |
NUXT_PUBLIC_LOCAL_AUTH_LOGOUT_ENDPOINT | Override default /auth/logout |
NUXT_PUBLIC_LOCAL_AUTH_REFRESH_ENDPOINT | Override default /auth/refresh |
NUXT_PUBLIC_LOCAL_AUTH_USER_ENDPOINT | Override default /auth/me |
NUXT_PUBLIC_LOCAL_AUTH_FORGOT_PASSWORD_ENDPOINT | Override default /auth/forgot-password |
NUXT_PUBLIC_LOCAL_AUTH_RESET_PASSWORD_ENDPOINT | Override default /auth/reset-password |
NUXT_PUBLIC_LOCAL_AUTH_CHANGE_PASSWORD_ENDPOINT | Override default /auth/change-password |
How It Works
On first call, useXAuth reads runtimeConfig.public.localAuth to resolve baseUrl and all endpoint paths. State (user, isLoading, emailSent) lives in Nuxt's useState so it is shared across all component instances without a Pinia store. fetchApi is an internal helper that injects the Bearer token header, handles 401 by attempting a single token refresh, then retries — callers never need to handle token expiry manually. forgotPassword always shows a success toast regardless of whether the email exists, preventing email enumeration. All redirect paths are validated against a safe-redirect guard before calling navigateTo.
