@workmate/nuxt-auth
| Install | |
|---|---|
npm install @workmate/nuxt-auth |
|
| Latest Version: | 3.0.1 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 12, 2026 |
| Links: | GitHub · npm |
@workmate/nuxt-auth
Auth module for Nuxt 3 & 4 apps. Supports local credentials, GitHub OAuth, and Google OAuth with Zod schema validation.
Providers
| Provider | Key | Status |
|---|---|---|
| Local | local | ✅ |
| GitHub | github | ✅ |
| ✅ | ||
| 🚧 | ||
| 🚧 |
Installation
npm install @workmate/nuxt-auth zod
# or
yarn add @workmate/nuxt-auth zod
zod is a peer dependency. The module needs zod@^4.2.0.
Add to nuxt.config.ts:
export default defineNuxtConfig({
modules: ["@workmate/nuxt-auth"],
});
Configuration
// nuxt.config.ts
import { z } from "zod";
const userSchema = z.object({
id: z.string(),
email: z.email(),
name: z.string().optional(),
});
export default defineNuxtConfig({
modules: ["@workmate/nuxt-auth"],
auth: {
global: false,
redirects: {
redirectIfNotLoggedIn: "/login",
redirectIfLoggedIn: "/",
},
apiClient: {
baseURL: "http://localhost:8080",
},
token: {
type: "Bearer",
maxAge: 1000 * 60 * 60 * 24 * 30,
cookiesNames: {
accessToken: "auth:token",
refreshToken: "auth:refreshToken",
authProvider: "auth:provider",
tokenType: "auth:tokenType",
},
},
providers: {
local: {
endpoints: {
signIn: {
path: "/api/auth/login/password",
method: "POST",
tokenKey: "token",
refreshTokenKey: "refresh_token",
},
signOut: { path: "/api/auth/logout", method: "POST" },
user: { path: "/api/auth/user", userKey: "user" },
refreshToken: {
path: "/api/auth/refresh",
method: "POST",
tokenKey: "token",
refreshTokenKey: "refresh_token",
body: { token: "token", refreshToken: "refresh_token" },
},
},
schemas: {
login: () => z.object({
email_address: z.email(),
password: z.string().min(8),
}),
user: () => userSchema,
},
},
github: {
CLIENT_ID: process.env.GITHUB_CLIENT_ID || "",
CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET || "",
HASHING_SECRET: process.env.HASHING_SECRET || "",
SCOPES: "user repo",
schemas: { user: () => userSchema },
},
google: {
CLIENT_ID: process.env.GOOGLE_CLIENT_ID || "",
CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET || "",
HASHING_SECRET: process.env.HASHING_SECRET || "",
SCOPES:
"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
schemas: { user: () => userSchema },
},
},
},
});
Module options reference
interface ModuleOptions {
global: boolean;
redirects: {
redirectIfNotLoggedIn?: string;
redirectIfLoggedIn?: string;
};
apiClient: {
baseURL: string;
credentials?: RequestCredentials;
};
token: {
type: string;
maxAge: number;
cookiesNames: {
accessToken: string;
refreshToken: string;
authProvider: string;
tokenType: string;
};
};
providers: {
local?: LocalAuthInitializerOptions;
github?: GithubAuthInitializerOptions;
google?: GoogleAuthInitializerOptions;
};
}
type HttpMethod =
| "GET"
| "HEAD"
| "PATCH"
| "POST"
| "PUT"
| "DELETE"
| "CONNECT"
| "OPTIONS"
| "TRACE";
type LocalAuthInitializerOptions = {
endpoints?: {
signIn?: {
path?: string;
method?: HttpMethod;
tokenKey?: string;
refreshTokenKey?: string;
};
signOut?: { path: string; method: HttpMethod } | false;
signUp?: { path?: string; method?: HttpMethod } | false;
user?: { path: string; userKey: string } | false;
refreshToken?:
| {
path: string;
method: HttpMethod;
tokenKey: string;
refreshTokenKey: string;
body: { token: string; refreshToken: string };
}
| false;
};
schemas?: {
login?: () => ZodType;
user?: () => ZodType;
};
};
type GithubAuthInitializerOptions = {
CLIENT_ID: string;
CLIENT_SECRET: string;
HASHING_SECRET: string;
SCOPES?: string;
schemas?: { user?: () => ZodType };
};
type GoogleAuthInitializerOptions = {
CLIENT_ID: string;
CLIENT_SECRET: string;
HASHING_SECRET: string;
SCOPES?: string;
schemas?: { user?: () => ZodType };
};
Server routes
The module registers these routes on your Nuxt server:
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/login | Login with any provider |
| POST | /api/auth/logout | Clear session and cookies |
| GET | /api/auth/user | Get the current authenticated user |
| POST | /api/auth/refresh | Refresh access token |
| GET | /api/auth/callback/github | GitHub OAuth callback (if configured) |
| GET | /api/auth/callback/google | Google OAuth callback (if configured) |
POST /api/auth/login
// Request body
{
provider: string; // "local" | "github" | "google"
[key: string]: https://github.com/work-mate/nuxt-auth-module/blob/HEAD/any; // provider-specific fields (e.g. email_address, password)
}
// Response — local provider
{
tokens: {
accessToken: string;
refreshToken?: string;
tokenType: string;
provider: string;
};
}
// Response — OAuth providers
{
url: string; // redirect to this URL to begin OAuth flow
}
POST /api/auth/logout
No request body. Clears all auth cookies and calls the provider's logout endpoint if configured.
GET /api/auth/user
No request body. Reads token from cookies.
// Response
{
user: AuthUser | null;
}
POST /api/auth/refresh
No request body. Reads tokens from cookies, calls the provider's refresh endpoint.
// Response
{
tokens: {
accessToken: string;
refreshToken?: string;
tokenType: string;
provider: string;
};
}
OAuth callbacks
Add <base_url>/api/auth/callback/github and <base_url>/api/auth/callback/google as the callback URLs in your OAuth app settings.
Composables
All composables are auto-imported.
useAuth()
Low-level access to the full auth plugin. Prefer the focused composables below for most use cases.
const {
isLoggedIn,
user,
token,
refreshToken,
tokenType,
provider,
tokenNames,
login,
logout,
refreshUser,
refreshTokens,
} = useAuth();
// login(provider, data?, redirectTo?)
await login("local", { email_address: "me@example.com", password: "secret" });
await login("github");
// logout(redirectTo?)
await logout("/login");
useAuthLogin()
Typed login functions. The local provider accepts any object — whatever you pass is sent directly to your signIn endpoint. If schemas.login is configured, the object is validated against it first; if not, it is forwarded as-is. You control the field names.
const { localLogin, googleLogin, githubLogin } = useAuthLogin();
// localLogin(opts, redirectTo?)
// opts shape comes entirely from your schemas.login Zod schema (or Record<string, any> if no schema)
await localLogin({ email_address: "me@example.com", password: "secret123" });
await localLogin({ username: "alice", pin: "1234" }); // different backend, different fields
await localLogin(
{ email_address: "me@example.com", password: "secret123" },
"/dashboard",
);
// googleLogin(opts?, redirectTo?)
await googleLogin();
await googleLogin({ redirectUrl: "/dashboard" });
// githubLogin(opts?, redirectTo?)
await githubLogin();
useAuthUser()
const { user, isLoggedIn, refreshUser } = useAuthUser();
// user: ComputedRef<AuthUser | null | undefined>
// isLoggedIn: ComputedRef<boolean>
if (isLoggedIn.value) {
console.log(user.value);
}
await refreshUser(); // re-fetches user from the server
useAuthToken()
const { token, refreshToken, tokenType, provider, tokenNames, refreshTokens } =
useAuthToken();
// All values are computed refs
console.log(token.value); // current access token
console.log(provider.value); // "local" | "github" | "google"
await refreshTokens(); // exchange refresh token for new tokens
useAuthFetch()
Wrapper around Nuxt's useFetch that automatically injects the Authorization header. Retries once on 401 after refreshing tokens. It has the same arguments and return type as useFetch.
const { data, error } = await useAuthFetch("/api/profile");
// Supports all useFetch options
const { data } = await useAuthFetch<User>("/api/profile", {
method: "POST",
body: { name: "Alice" },
});
Plugins
$auth
The full auth plugin, same as useAuth().
const { $auth } = useNuxtApp();
if ($auth.isLoggedIn.value) {
await $auth.logout();
}
$authFetch
An ofetch instance pre-configured with your apiClient.baseURL, the current Authorization header, and automatic token refresh on 401. Use this instead of $fetch for authenticated requests.
const { $authFetch } = useNuxtApp();
const data = await $authFetch("/api/protected");
To set the base URL:
auth: {
apiClient: {
baseURL: "http://localhost:8080/v1",
},
}
Set apiClient.credentials to "include" when the API is on another origin and requests must carry cookies (for example load-balancer sticky-session cookies). The API must then respond with Access-Control-Allow-Credentials: true and an exact Access-Control-Allow-Origin — a * wildcard is rejected by browsers for credentialed requests.
auth: {
apiClient: {
baseURL: "https://api.example.com",
credentials: "include",
},
}
Middlewares
Route middleware
Protect a page (requires login):
// pages/dashboard.vue
definePageMeta({
middleware: "auth",
});
Guest-only page (redirect away if logged in):
// pages/login.vue
definePageMeta({
middleware: "auth-guest",
});
Global middleware
Apply auth to all routes:
// nuxt.config.ts
auth: {
global: true,
}
Opt a page out when global is enabled:
definePageMeta({
auth: false,
});
Redirects are configured under auth.redirects:
redirectIfNotLoggedIn— whereauthmiddleware sends unauthenticated users (default:"/login")redirectIfLoggedIn— whereauth-guestmiddleware sends authenticated users (default:"/")
Schema validation
Schemas are defined in nuxt.config.ts per provider using Zod 4.2 or later. They are converted to JSON Schema at build time and reconstructed at runtime — no serialization issues.
Each schema is a function that returns the Zod schema. Nuxt 4.5 and later deep-clone nuxt.config.ts, and a Zod schema that is put directly in the config stops the build with Cannot read properties of undefined (reading 'checks').
// nuxt.config.ts
import { z } from "zod";
local: {
schemas: {
login: () => z.object({ email_address: z.email(), password: z.string().min(8) }),
user: () => z.object({ id: z.string(), email: z.email(), name: z.string().optional() }),
},
}
schemas.login — the local provider sends whatever object you pass directly to your signIn endpoint. There are no fixed field names — use whatever your backend expects (email_address, username, phone, etc.). If schemas.login is configured, the body is validated server-side before being forwarded. Invalid bodies return HTTP 400 with field errors:
{
"message": "Invalid request body",
"data": { "password": ["Too small: expected string to have >=8 characters"] }
}
If schemas.login is not configured, the body is forwarded as-is with no validation.
schemas.user — validated server-side when your backend returns user data. Mismatches throw a server error with field-level detail. Optional — omit to skip validation.
A schema must be representable as JSON Schema. A schema with .transform() or z.date() stops the build. A .refine() check is not kept, because JSON Schema cannot hold a function.
TypeScript infers the login payload type from your schema — localLogin(opts) is typed as the Zod output of schemas.login, or Record<string, any> if no schema is set.
Migration: v2 → v3 (breaking changes in v3.0.0)
Schemas are functions
Each schema in nuxt.config.ts is a function that returns the Zod schema. A schema that is not a function stops the build.
Before:
schemas: {
login: z.object({ email_address: z.email(), password: z.string().min(8) }),
user: userSchema,
}
After:
schemas: {
login: () => z.object({ email_address: z.email(), password: z.string().min(8) }),
user: () => userSchema,
}
Zod is a peer dependency
The module uses the zod package of your app. Install zod@^4.2.0 in your app:
npm install zod@^4.2.0
defaultProvider is removed
The defaultProvider option had no effect. Remove it from the auth config.
useAuthFetch has the type of useFetch
useAuthFetch accepts the same arguments as useFetch and returns the same type. When you give no type argument, the type of data comes from the server route, as it does for useFetch.
Migration: v1 → v2 (breaking changes in v2.0.0)
Configuration
In v1, signIn.body mapped fixed field names (principal, password) onto the request. In v2 the body is open — you pass any object and it is sent as-is to your endpoint. Remove signIn.body entirely and add schemas.login to describe the shape your backend expects.
Before:
local: {
endpoints: {
signIn: {
path: "/api/auth/login",
tokenKey: "token",
body: {
principal: "email_address",
password: "password",
},
},
},
}
After:
import { z } from "zod";
local: {
endpoints: {
signIn: {
path: "/api/auth/login",
tokenKey: "token",
},
},
schemas: {
login: () => z.object({
email_address: z.email(),
password: z.string().min(8),
}),
},
}
Login call
Before:
const { login } = useAuth();
await login("local", { principal: values.email, password: values.password });
After:
const { localLogin } = useAuthLogin();
await localLogin({ email_address: values.email, password: values.password });
Peer dependency
Zod 4.2 or later is required (zod@^4.2.0). If you were on Zod 3, upgrade:
npm install zod@^4.2.0
z.fromJSONSchema does not exist before Zod 4.2.
Related Packages
Nuxt 4 module providing OpenID Connect / OAuth 2.0 authorization code grant with...
JWT-based SSO bridge client for Nuxt 3. Connects to external JWT authentication...
Nuxt module that signs your app in with Authup and guards its routes by login st...
Auto-generate Nitro defineRouteMeta (OpenAPI) for Nuxt server routes from JSDoc...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 3.0.1 | ^3.0 | MIT | |
| 3.0.0 | ^3.0 | MIT | |
| 2.2.0 | ^3.0 | MIT | |
| 2.1.0 | ^3.0 | MIT | |
| 2.0.1 | ^3.0 | MIT | |
| 2.0.0 | ^3.0 | MIT | |
| 1.6.2 | ^3.0 | MIT | |
| 1.6.1 | ^3.0 | MIT | |
| 1.5.2 | ^3.0 | MIT | |
| 1.5.1 | ^3.0 | MIT | |
| 1.5.0 | ^3.0 | MIT | |
| 1.4.0 | ^3.0 | MIT | |
| 1.3.2 | ^3.0 | MIT | |
| 1.3.1 | ^3.0 | MIT | |
| 1.3.0 | ^3.0 | MIT | |
| 1.2.0 | ^3.0 | MIT | |
| 1.0.2 | ^3.0 | MIT | |
| 1.0.1 | ^3.0 | MIT |
Pre-releases (13)
| Version | Released | Vue | License |
|---|---|---|---|
| 1.0.0-beta-2.7 | ^3.0 | MIT | |
| 1.0.0-beta-2.6 | ^3.0 | MIT | |
| 1.0.0-beta-2.5 | ^3.0 | MIT | |
| 1.0.0-beta-2.4 | ^3.0 | MIT | |
| 1.0.0-beta-2.3 | ^3.0 | MIT | |
| 1.0.0-beta-2.2 | ^3.0 | MIT | |
| 1.0.0-beta-2.1 | ^3.0 | MIT | |
| 1.0.0-beta-2 | ^3.0 | MIT | |
| 1.0.0-beta-1 | ^3.0 | MIT | |
| 1.0.0-alpha.0.4 | ^3.0 | MIT | |
| 1.0.0-alpha.0.3 | ^3.0 | MIT | |
| 1.0.0-alpha.0.2 | ^3.0 | MIT | |
| 1.0.0-alpha.0.1 | ^3.0 | MIT |