@artatol-acp/auth-nuxt
v0.5.0
Published
Nuxt module for Artatol Cloud Platform Authentication with auto-imports, composables, and server middleware
Maintainers
Readme
@artatol-acp/auth-nuxt
Nuxt module for Artatol Cloud Platform Authentication with auto-imports, composables, server middleware, and automatic token refresh.
Changelog
v0.4.2
Features:
- SSO support for artatol.net: On
*.artatol.netsubdomains, browser receivesrefresh_tokencookie directly from auth server (native SSO). On other domains, SDK extracts it from Set-Cookie header and stores it locally.
v0.4.1
Bug Fixes:
- Fixed refresh token extraction: Auth server returns
refresh_tokenonly inSet-Cookieheader, not in JSON body. SDK now correctly extracts it from the response header. - Fixed cookie forwarding: Refresh and logout endpoints now properly forward the
refresh_tokencookie to the auth server.
v0.4.0
Improvements:
- Updated to use
@artatol-acp/auth-jsv0.4.0 with improved error handling - Better error messages: ACPAuthError now always contains proper error message from API
- New error helper methods:
isAuthError(),isValidationError(),isNetworkError() apiKeyis now optional in configuration
Installation
npm install @artatol-acp/auth-nuxt
# or
pnpm add @artatol-acp/auth-nuxtPrerequisites
Before using this module, you need to obtain from the ACP AUTH service:
API Key (required) - Contact your system administrator
Base URL (required) - The auth service URL (e.g.,
https://sso.artatol.net)JWT Public Key (optional) - Only needed for local JWT verification with
/api/auth/user. Download it from:curl https://sso.artatol.net/public-keyNote: Without the public key, you can still use all auth operations (login, register, logout, 2FA, password reset, etc.), but you must use
/api/auth/meinstead of/api/auth/userfor user verification, which makes an API call to the auth service.
Setup
Add the module to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@artatol-acp/auth-nuxt'],
acpAuth: {
baseUrl: 'https://sso.artatol.net',
apiKey: process.env.ACP_AUTH_API_KEY,
jwtPublicKey: `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`,
enableMiddleware: true,
publicPaths: ['/login', '/register', '/forgot-password'],
loginPath: '/login',
},
});Usage
Composables
useAuth()
Access auth functionality:
<script setup>
const { login, logout, refresh, register, verifyEmail, resendVerificationEmail, forgotPassword, resetPassword, deleteAccount, client } = useAuth();
const handleLogin = async () => {
const result = await login({
email: '[email protected]',
password: 'password123',
});
if ('requiresTwoFactor' in result) {
// Handle 2FA
console.log(result.tempToken);
} else {
// Login successful
console.log(result.user);
}
};
</script>
<template>
<button @click="handleLogin">Login</button>
<button @click="logout">Logout</button>
</template>useUser()
Get current user (fast, local JWT verification). Returns basic user info without 2FA status.
<script setup>
const { user, isLoading, refresh } = useUser();
// user: { id: string, email: string } | null
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="user">
Welcome {{ user.email }}
</div>
<div v-else>Not logged in</div>
</template>useMe()
Get current user with full data including 2FA status (API call).
<script setup>
const { user, isLoading, refresh } = useMe();
// user: { id: string, email: string, twoFactorEnabled: boolean } | null
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="user">
Welcome {{ user.email }}
<span v-if="user.twoFactorEnabled">2FA enabled</span>
</div>
<div v-else>Not logged in</div>
</template>Server API Routes
The module automatically adds these API routes:
POST /api/auth/login- Login userPOST /api/auth/logout- Logout userPOST /api/auth/refresh- Refresh access tokenGET /api/auth/user- Get current user (verifies locally without API call)GET /api/auth/me- Get current user from API (validates token with auth service)
Middleware
If enableMiddleware is enabled, the module will protect all routes except public paths.
To disable middleware for a specific route:
definePageMeta({
middleware: 'guest', // or custom middleware
});Direct Client Access
You can also use the ACP Auth client directly:
<script setup>
const { $acpAuth } = useNuxtApp();
const register = async () => {
await $acpAuth.register({
email: '[email protected]',
password: 'password123',
});
// User will receive verification email
};
</script>Password Requirements
Passwords must meet the following requirements:
- Minimum 10 characters
- At least one lowercase letter (a-z)
- At least one uppercase letter (A-Z)
- At least one number (0-9)
<script setup>
function validatePassword(password: string): string[] {
const errors: string[] = [];
if (password.length < 10) {
errors.push('Password must be at least 10 characters');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain at least one number');
}
return errors;
}
</script>Email Verification
After registration, users must verify their email address before they can log in. The auth service automatically sends a verification email upon registration.
Verification Flow
- User registers → receives verification email
- User clicks link in email → email is verified
- User can now log in
Using Composables
<script setup>
const { $acpAuth } = useNuxtApp();
const route = useRoute();
// Verify email from token in URL
const verifyEmail = async () => {
const token = route.query.token as string;
try {
await $acpAuth.verifyEmail({ token });
navigateTo('/login?verified=true');
} catch (error) {
console.error('Verification failed:', error);
}
};
// Resend verification email
const resendVerification = async (email: string) => {
await $acpAuth.resendVerificationEmail({ email });
// Always succeeds to prevent email enumeration
};
</script>
<template>
<button @click="verifyEmail">Verify Email</button>
<button @click="resendVerification('[email protected]')">
Resend Verification Email
</button>
</template>Handling Unverified Users
When an unverified user tries to log in, they will receive an error:
<script setup>
const { login } = useAuth();
const errorMessage = ref('');
const handleLogin = async (email: string, password: string) => {
try {
const result = await login({ email, password });
if ('requiresTwoFactor' in result) {
// Handle 2FA
} else {
navigateTo('/dashboard');
}
} catch (error: any) {
if (error.message?.includes('Email not verified')) {
errorMessage.value = 'Please verify your email before logging in';
} else {
errorMessage.value = 'Login failed';
}
}
};
</script>Server API Routes
The module automatically adds email verification routes:
POST /api/auth/verify-email- Verify user's email addressPOST /api/auth/resend-verification- Resend verification email
Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| baseUrl | string | required | Base URL of ACP AUTH service |
| apiKey | string | required | API key for authenticating your application |
| jwtPublicKey | string | optional | EdDSA public key for JWT verification |
| enableMiddleware | boolean | true | Enable auth middleware |
| publicPaths | string[] | ['/login', '/register', '/forgot-password', '/reset-password'] | Paths that don't require auth |
| loginPath | string | '/login' | Login page path |
TypeScript
The module includes TypeScript types. All composables and the client are fully typed.
2FA (Two-Factor Authentication)
Setup 2FA
<script setup>
const { $acpAuth } = useNuxtApp();
const password = ref('');
const qrCodeUrl = ref('');
const recoveryCodes = ref([]);
const setup2FA = async () => {
const accessToken = // ... get from state/cookie
const result = await $acpAuth.setup2FA({ password: password.value }, accessToken);
qrCodeUrl.value = result.qrCodeUrl;
recoveryCodes.value = result.recoveryCodes;
};
</script>
<template>
<div>
<input v-model="password" type="password" placeholder="Your password" />
<button @click="setup2FA">Setup 2FA</button>
<div v-if="qrCodeUrl">
<img :src="qrCodeUrl" alt="QR Code" />
<p>Scan this QR code with your authenticator app</p>
<h3>Recovery Codes</h3>
<ul>
<li v-for="code in recoveryCodes" :key="code">{{ code }}</li>
</ul>
</div>
</div>
</template>Verify 2FA Setup
<script setup>
const { $acpAuth } = useNuxtApp();
const code = ref('');
const verify2FA = async () => {
const accessToken = // ... get from state/cookie
await $acpAuth.verify2FA({ code: code.value }, accessToken);
// 2FA is now enabled
navigateTo('/dashboard');
};
</script>
<template>
<div>
<input v-model="code" placeholder="6-digit code" maxlength="6" />
<button @click="verify2FA">Verify & Enable 2FA</button>
</div>
</template>Complete 2FA Login Flow
<script setup>
const { login } = useAuth();
const email = ref('');
const password = ref('');
const requires2FA = ref(false);
const tempToken = ref('');
const twoFactorCode = ref('');
const handleLogin = async () => {
const result = await login({
email: email.value,
password: password.value,
});
if ('requiresTwoFactor' in result) {
requires2FA.value = true;
tempToken.value = result.tempToken;
} else {
// Login successful
navigateTo('/dashboard');
}
};
const complete2FA = async () => {
const { $acpAuth } = useNuxtApp();
await $acpAuth.verify2FALogin({
tempToken: tempToken.value,
code: twoFactorCode.value,
});
navigateTo('/dashboard');
};
</script>
<template>
<div v-if="!requires2FA">
<input v-model="email" type="email" placeholder="Email" />
<input v-model="password" type="password" placeholder="Password" />
<button @click="handleLogin">Login</button>
</div>
<div v-else>
<input v-model="twoFactorCode" placeholder="6-digit code" maxlength="6" />
<button @click="complete2FA">Verify Code</button>
</div>
</template>Disable 2FA
<script setup>
const { $acpAuth } = useNuxtApp();
const password = ref('');
const code = ref('');
const disable2FA = async () => {
const accessToken = // ... get from state/cookie
await $acpAuth.disable2FA({
password: password.value,
code: code.value,
}, accessToken);
// 2FA is now disabled
};
</script>
<template>
<div>
<input v-model="password" type="password" placeholder="Your password" />
<input v-model="code" placeholder="Current 2FA code" maxlength="6" />
<button @click="disable2FA">Disable 2FA</button>
</div>
</template>Registration
Register User
<script setup>
const { register } = useAuth();
const email = ref('');
const password = ref('');
const registered = ref(false);
const handleRegister = async () => {
await register({ email: email.value, password: password.value });
// User will receive verification email
registered.value = true;
};
</script>
<template>
<div v-if="!registered">
<input v-model="email" type="email" placeholder="Email" />
<input v-model="password" type="password" placeholder="Password" />
<button @click="handleRegister">Register</button>
</div>
<div v-else>
Check your email to verify your account.
</div>
</template>Password Reset
Forgot Password
Request a password reset email:
<script setup>
const { forgotPassword } = useAuth();
const email = ref('');
const submitted = ref(false);
const handleForgotPassword = async () => {
await forgotPassword(email.value);
// Always succeeds to prevent email enumeration
submitted.value = true;
};
</script>
<template>
<div v-if="!submitted">
<input v-model="email" type="email" placeholder="Email" />
<button @click="handleForgotPassword">Send Reset Link</button>
</div>
<div v-else>
Check your email for a password reset link.
</div>
</template>Reset Password
Reset password using the token from the email:
<script setup>
const { resetPassword } = useAuth();
const route = useRoute();
const newPassword = ref('');
const confirmPassword = ref('');
const error = ref('');
const handleResetPassword = async () => {
const token = route.query.token as string;
if (newPassword.value !== confirmPassword.value) {
error.value = 'Passwords do not match';
return;
}
try {
await resetPassword(token, newPassword.value);
navigateTo('/login?reset=success');
} catch (e) {
error.value = 'Invalid or expired reset token';
}
};
</script>
<template>
<div>
<div v-if="error" class="error">{{ error }}</div>
<input v-model="newPassword" type="password" placeholder="New Password" />
<input v-model="confirmPassword" type="password" placeholder="Confirm Password" />
<button @click="handleResetPassword">Reset Password</button>
</div>
</template>Delete Account
Delete the authenticated user's account:
<script setup>
const { deleteAccount } = useAuth();
const password = ref('');
const confirmation = ref('');
const error = ref('');
const handleDeleteAccount = async () => {
if (confirmation.value !== 'DELETE') {
error.value = 'Please type DELETE to confirm';
return;
}
try {
const accessToken = // ... get from state/cookie
await deleteAccount(password.value, confirmation.value, accessToken);
navigateTo('/goodbye');
} catch (e) {
error.value = 'Failed to delete account. Check your password.';
}
};
</script>
<template>
<div>
<div v-if="error" class="error">{{ error }}</div>
<input v-model="password" type="password" placeholder="Your password" />
<input v-model="confirmation" placeholder="Type DELETE to confirm" />
<button @click="handleDeleteAccount">Delete My Account</button>
</div>
</template>Note: The confirmation parameter must be the string "DELETE" to confirm account deletion.
Health Check
To check if the auth service is available:
<script setup>
const { $acpAuth } = useNuxtApp();
const healthStatus = ref(null);
onMounted(async () => {
const health = await $acpAuth.health();
healthStatus.value = health;
console.log(health); // { status: 'ok', timestamp: '...' }
});
</script>
<template>
<div v-if="healthStatus">
Auth service: {{ healthStatus.status }}
</div>
</template>Automatic Token Refresh
The Nuxt module automatically refreshes access tokens in the background to maintain seamless user sessions.
How It Works
- A client-side plugin runs automatically when your app loads
- Every 4 minutes, it calls
/api/auth/refreshto get a new access token - The refresh happens silently in the background
- If refresh fails, the user session is considered expired
- Access tokens expire after 5 minutes, so refreshing every 4 minutes ensures tokens never expire during active sessions
Manual Control
You can manually control the auto-refresh behavior:
<script setup>
const { $authRefresh } = useNuxtApp();
// Stop auto-refresh (e.g., when user logs out)
onUnmounted(() => {
$authRefresh.stop();
});
// Restart auto-refresh (e.g., after login)
const handleLogin = async () => {
await login({ email: '...', password: '...' });
$authRefresh.start();
};
</script>Manual Refresh
You can also manually refresh tokens using the useAuth() composable:
<script setup>
const { refresh } = useAuth();
// Manually refresh the token
const handleRefresh = async () => {
try {
await refresh();
console.log('Token refreshed successfully');
} catch (error) {
console.error('Failed to refresh token');
}
};
</script>Error Handling
<script setup>
import { ACPAuthError } from '@artatol-acp/auth-js';
const { login } = useAuth();
const errorMessage = ref('');
const handleLogin = async (email: string, password: string) => {
try {
await login({ email, password });
navigateTo('/dashboard');
} catch (error) {
if (error instanceof ACPAuthError) {
console.error('Auth error:', error.message);
console.error('Status code:', error.statusCode);
if (error.statusCode === 401) {
errorMessage.value = 'Invalid credentials';
} else if (error.statusCode === 403) {
errorMessage.value = 'Please verify your email or check if your account is locked';
} else if (error.statusCode === 429) {
errorMessage.value = 'Too many attempts. Please try again later';
} else {
errorMessage.value = error.message;
}
} else {
errorMessage.value = 'An unexpected error occurred';
}
}
};
</script>
<template>
<div v-if="errorMessage" class="error">
{{ errorMessage }}
</div>
</template>Common Error Codes
| Status Code | Meaning | |-------------|---------| | 401 | Unauthorized (invalid credentials or token) | | 403 | Forbidden (email not verified, account locked) | | 429 | Too Many Requests (rate limited) | | 500 | Internal Server Error |
License
MIT
