@ovixa/auth-client
v0.4.2
Published
Client SDK for Ovixa Auth service
Maintainers
Readme
@ovixa/auth-client
Client SDK for the Ovixa Auth service. Provides authentication, token verification, and session management for applications using Ovixa's centralized identity provider.
Features
- Email/password authentication (signup, login, password reset)
- Passkey (WebAuthn) authentication - phishing-resistant passwordless sign-in
- OAuth integration (Google, GitHub)
- JWT verification with JWKS caching
- Automatic token refresh
- Framework integrations (Astro, Express)
- TypeScript-first with full type definitions
Installation
npm install @ovixa/auth-client
# or
pnpm add @ovixa/auth-clientQuick Start
import { OvixaAuth } from '@ovixa/auth-client';
const auth = new OvixaAuth({
authUrl: 'https://auth.example.com',
realmId: 'your-realm-id',
});
// Sign up a new user
await auth.signup({
email: '[email protected]',
password: 'SecurePassword123!',
redirectUri: 'https://yourapp.com/verify-callback',
});
// Log in and get tokens
const tokens = await auth.login({
email: '[email protected]',
password: 'SecurePassword123!',
});
// Convert to AuthResult for easier handling
const result = await auth.toAuthResult(tokens);
console.log('User:', result.user.email);
console.log('Expires:', result.session.expiresAt);
// Verify a JWT token
const verified = await auth.verifyToken(tokens.access_token);
console.log('User ID:', verified.payload.sub);API Reference
OvixaAuth
Main client class for interacting with the Ovixa Auth service.
Constructor
new OvixaAuth(config: AuthClientConfig)| Option | Type | Required | Description |
| -------------- | -------- | -------- | ------------------------------------------- |
| authUrl | string | Yes | Base URL of the Ovixa Auth service |
| realmId | string | Yes | The realm ID to authenticate against |
| clientSecret | string | No | Client secret (for server-side use) |
| jwksCacheTtl | number | No | JWKS cache duration in ms (default: 1 hour) |
Authentication Methods
signup(options)
Create a new user account. A verification email is sent after signup.
await auth.signup({
email: '[email protected]',
password: 'SecurePassword123!',
redirectUri: 'https://yourapp.com/verify-callback', // Optional
});login(options)
Authenticate with email and password.
const tokens = await auth.login({
email: '[email protected]',
password: 'SecurePassword123!',
});
// Returns: { access_token, refresh_token, token_type, expires_in }logout(refreshToken)
Revoke a refresh token to log out.
await auth.logout(refreshToken);Email Verification
verifyEmail(options)
Verify email using a token (returns tokens for automatic login).
const tokens = await auth.verifyEmail({
token: 'verification-token-from-email',
});resendVerification(options)
Resend the verification email.
await auth.resendVerification({
email: '[email protected]',
redirectUri: 'https://yourapp.com/verify-callback', // Optional
});Password Reset
forgotPassword(options)
Request a password reset email.
await auth.forgotPassword({
email: '[email protected]',
redirectUri: 'https://yourapp.com/reset-password', // Optional
});Email Branding
Verification and password reset emails automatically display your realm's display_name as the brand name. To customize the branding in emails:
- Set
display_namewhen creating your realm - Emails will show your brand (e.g., "Linkdrop") instead of "Ovixa"
If no display_name is set, emails default to "Ovixa".
resetPassword(options)
Set a new password using a reset token.
await auth.resetPassword({
token: 'reset-token-from-email',
password: 'NewSecurePassword123!',
});Token Management
verifyToken(token)
Verify an access token and return the decoded payload.
const result = await auth.verifyToken(accessToken);
console.log('User ID:', result.payload.sub);
console.log('Email:', result.payload.email);
console.log('Verified:', result.payload.email_verified);refreshToken(refreshToken)
Exchange a refresh token for new tokens.
const newTokens = await auth.refreshToken(currentRefreshToken);
// Store the new tokens - refresh token rotation is usedtoAuthResult(tokenResponse)
Convert a token response to a structured AuthResult with user and session data.
const tokens = await auth.login({ email, password });
const result = await auth.toAuthResult(tokens);
// result.user: { id, email, emailVerified }
// result.session: { accessToken, refreshToken, expiresAt }
// result.isNewUser?: boolean (for OAuth flows)clearJwksCache()
Invalidate the cached JWKS to force a refresh on next verification.
auth.clearJwksCache();Admin Operations
Admin operations require clientSecret to be configured. These operations allow server-side management of users within the realm boundary.
Important: Never expose clientSecret to client-side code.
admin.deleteUser(options)
Delete a user from your realm. This permanently deletes the user and all associated data (tokens, OAuth accounts).
const auth = new OvixaAuth({
authUrl: 'https://auth.example.com',
realmId: 'your-realm-id',
clientSecret: process.env.OVIXA_CLIENT_SECRET, // Required
});
// Delete a user
await auth.admin.deleteUser({ userId: 'user-id-to-delete' });Error Handling:
try {
await auth.admin.deleteUser({ userId });
} catch (error) {
if (error instanceof OvixaAuthError) {
if (error.code === 'NOT_FOUND') {
console.error('User not found');
} else if (error.code === 'FORBIDDEN') {
console.error('User does not belong to this realm');
} else if (error.code === 'REALM_UNAUTHORIZED') {
console.error('Invalid client secret');
}
}
}Passkeys (WebAuthn)
Passkeys provide phishing-resistant, passwordless authentication using FIDO2/WebAuthn.
webauthn.getRegistrationOptions(options)
Get options for registering a new passkey. Requires authentication.
const options = await auth.webauthn.getRegistrationOptions({
accessToken: 'user-access-token',
});
// Returns PublicKeyCredentialCreationOptions for navigator.credentials.create()webauthn.verifyRegistration(options)
Verify and store a new passkey registration. Requires authentication.
const result = await auth.webauthn.verifyRegistration({
accessToken: 'user-access-token',
registration: credentialResponse, // From navigator.credentials.create()
deviceName: 'My MacBook', // Optional friendly name
});
// Returns: { success: true, credential_id: string, device_name?: string }webauthn.getAuthenticationOptions(options)
Get options for signing in with a passkey. Does not require authentication.
const options = await auth.webauthn.getAuthenticationOptions({
email: '[email protected]', // Optional - provides allowCredentials hint
});
// Returns PublicKeyCredentialRequestOptions for navigator.credentials.get()webauthn.authenticate(options)
Verify passkey authentication and receive tokens.
const tokens = await auth.webauthn.authenticate({
authentication: credentialResponse, // From navigator.credentials.get()
});
// Returns: { access_token, refresh_token, token_type, expires_in }Complete Passkey Flow Example
import { OvixaAuth } from '@ovixa/auth-client';
const auth = new OvixaAuth({
authUrl: 'https://auth.example.com',
realmId: 'your-realm-id',
});
// Register a passkey (user must be logged in)
async function registerPasskey(accessToken: string) {
// 1. Get registration options from server
const options = await auth.webauthn.getRegistrationOptions({ accessToken });
// 2. Create credential using browser API
const credential = await navigator.credentials.create({
publicKey: options,
});
if (!credential) throw new Error('Registration cancelled');
// 3. Verify with server
const result = await auth.webauthn.verifyRegistration({
accessToken,
registration: credential as PublicKeyCredential,
deviceName: 'My Device',
});
return result;
}
// Sign in with passkey
async function signInWithPasskey(email?: string) {
// 1. Get authentication options
const options = await auth.webauthn.getAuthenticationOptions({ email });
// 2. Get credential using browser API
const credential = await navigator.credentials.get({
publicKey: options,
});
if (!credential) throw new Error('Authentication cancelled');
// 3. Verify and get tokens
const tokens = await auth.webauthn.authenticate({
authentication: credential as PublicKeyCredential,
});
return tokens;
}OAuth
getOAuthUrl(options)
Generate an OAuth authorization URL.
const googleUrl = auth.getOAuthUrl({
provider: 'google', // or 'github'
redirectUri: 'https://yourapp.com/auth/callback',
});
// Redirect user to start OAuth flow
window.location.href = googleUrl;After OAuth completes, the user is redirected to your redirectUri with tokens as URL hash parameters:
// Handle callback in your app
const hash = new URLSearchParams(window.location.hash.slice(1));
const accessToken = hash.get('access_token');
const refreshToken = hash.get('refresh_token');Framework Integrations
Astro Middleware
// src/middleware.ts
import { createAstroAuth } from '@ovixa/auth-client/astro';
import { OvixaAuth } from '@ovixa/auth-client';
const auth = new OvixaAuth({
authUrl: import.meta.env.AUTH_URL,
realmId: import.meta.env.AUTH_REALM_ID,
});
export const onRequest = createAstroAuth({
auth,
publicRoutes: ['/', '/login', '/signup', '/api/public/*'],
loginRedirect: '/login',
cookies: {
secure: import.meta.env.PROD,
},
});Access auth context in pages:
---
// src/pages/dashboard.astro
const { user, isAuthenticated } = Astro.locals.auth;
if (!isAuthenticated) {
return Astro.redirect('/login');
}
---
<h1>Welcome, {user.email}</h1>Set cookies after login:
// src/pages/api/login.ts
import { setAstroAuthCookies } from '@ovixa/auth-client/astro';
import type { APIContext } from 'astro';
export async function POST({ request, cookies }: APIContext) {
const { email, password } = await request.json();
const tokens = await auth.login({ email, password });
setAstroAuthCookies({ cookies }, tokens);
return new Response(JSON.stringify({ success: true }));
}Clear cookies on logout:
// src/pages/api/logout.ts
import { clearAstroAuthCookies } from '@ovixa/auth-client/astro';
export async function POST({ cookies, locals }: APIContext) {
if (locals.auth?.session?.refreshToken) {
await auth.logout(locals.auth.session.refreshToken);
}
clearAstroAuthCookies({ cookies });
return new Response(JSON.stringify({ success: true }));
}Express Middleware
import express from 'express';
import cookieParser from 'cookie-parser';
import { createExpressAuth, requireAuth } from '@ovixa/auth-client/express';
import { OvixaAuth } from '@ovixa/auth-client';
const auth = new OvixaAuth({
authUrl: process.env.AUTH_URL!,
realmId: process.env.AUTH_REALM_ID!,
});
const app = express();
app.use(cookieParser());
app.use(
createExpressAuth({
auth,
publicRoutes: ['/', '/login', '/signup'],
loginRedirect: '/login',
cookies: {
secure: process.env.NODE_ENV === 'production',
},
})
);
// Access auth context in routes
app.get('/api/me', (req, res) => {
if (!req.auth?.isAuthenticated) {
return res.status(401).json({ error: 'Unauthorized' });
}
res.json({ user: req.auth.user });
});
// Use requireAuth middleware for protected routes
app.get('/dashboard', requireAuth({ redirect: '/login' }), (req, res) => {
res.render('dashboard', { user: req.auth.user });
});Set cookies after login:
import { setExpressAuthCookies } from '@ovixa/auth-client/express';
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const tokens = await auth.login({ email, password });
setExpressAuthCookies(res, req, tokens);
res.json({ success: true });
});Custom Framework Integration
Implement the CookieAdapter interface to add support for other frameworks:
import type {
CookieAdapter,
SetCookieOptions,
DeleteCookieOptions,
} from '@ovixa/auth-client/astro';
class HonoCookieAdapter implements CookieAdapter {
constructor(private context: HonoContext) {}
getCookie(name: string): string | undefined {
return this.context.req.cookie(name);
}
setCookie(name: string, value: string, options: SetCookieOptions): void {
this.context.cookie(name, value, options);
}
deleteCookie(name: string, options: DeleteCookieOptions): void {
this.context.cookie(name, '', { ...options, maxAge: 0 });
}
}Middleware Configuration
| Option | Type | Default | Description |
| --------------- | ----------- | -------- | ---------------------------------------- |
| auth | OvixaAuth | Required | The OvixaAuth client instance |
| publicRoutes | string[] | [] | Routes that bypass authentication |
| loginRedirect | string | - | URL to redirect unauthenticated requests |
| autoRefresh | boolean | true | Automatically refresh expired tokens |
| cookies | object | - | Cookie configuration (see below) |
Cookie Options
| Option | Type | Default | Description |
| -------------------- | --------- | ----------------------- | ------------------------------- |
| accessTokenCookie | string | 'ovixa_access_token' | Name of access token cookie |
| refreshTokenCookie | string | 'ovixa_refresh_token' | Name of refresh token cookie |
| path | string | '/' | Cookie path |
| secure | boolean | true | Use secure cookies (HTTPS only) |
| httpOnly | boolean | true | HTTP-only cookies |
| sameSite | string | 'lax' | SameSite policy |
| domain | string | - | Cookie domain |
Error Handling
All methods throw OvixaAuthError on failure:
import { OvixaAuth, OvixaAuthError } from '@ovixa/auth-client';
try {
await auth.login({ email, password });
} catch (error) {
if (error instanceof OvixaAuthError) {
console.error('Code:', error.code); // e.g., 'INVALID_CREDENTIALS'
console.error('Message:', error.message);
console.error('Status:', error.statusCode); // HTTP status code
}
}Error Codes
| Code | Description |
| ------------------------- | ----------------------------------------------------- |
| INVALID_CREDENTIALS | Wrong email or password |
| EMAIL_NOT_VERIFIED | User must verify email before login |
| PASSWORD_RESET_REQUIRED | Account flagged for mandatory password reset |
| INVALID_TOKEN | Token is invalid or malformed |
| TOKEN_EXPIRED | Token has expired |
| INVALID_SIGNATURE | Token signature verification failed |
| INVALID_ISSUER | Token issuer doesn't match |
| INVALID_AUDIENCE | Token audience doesn't match |
| RATE_LIMITED | Too many requests |
| NETWORK_ERROR | Failed to reach auth service |
| BAD_REQUEST | Invalid request parameters |
| UNAUTHORIZED | Authentication required |
| FORBIDDEN | Access denied (e.g., user belongs to different realm) |
| NOT_FOUND | Resource not found |
| SERVER_ERROR | Auth service error |
| REALM_UNAUTHORIZED | Invalid or missing realm client secret |
| CLIENT_SECRET_REQUIRED | Admin operation attempted without clientSecret config |
Handling PASSWORD_RESET_REQUIRED
When an administrator flags an account as compromised, login and token refresh will return a 403 error with code PASSWORD_RESET_REQUIRED. The user must complete a password reset before they can log in again.
try {
const tokens = await auth.login({ email, password });
} catch (error) {
if (error instanceof OvixaAuthError) {
if (error.code === 'PASSWORD_RESET_REQUIRED') {
// Redirect user to password reset flow
await auth.forgotPassword({ email });
// Show message: "Your account requires a password reset. Check your email."
return;
}
// Handle other errors...
}
}This also applies to token refresh - if you're using automatic token refresh in middleware, handle this error to redirect users to the password reset flow:
try {
const newTokens = await auth.refreshToken(refreshToken);
} catch (error) {
if (error instanceof OvixaAuthError && error.code === 'PASSWORD_RESET_REQUIRED') {
// Clear cookies and redirect to login with a message
clearAuthCookies();
redirect('/login?reason=password_reset_required');
}
}Types
// Token response from auth service
interface TokenResponse {
access_token: string;
refresh_token: string;
token_type: 'Bearer';
expires_in: number;
is_new_user?: boolean; // OAuth flows only
}
// User information (extracted from JWT claims)
interface User {
id: string;
email: string;
emailVerified: boolean;
// Note: createdAt is intentionally omitted. The JWT `iat` claim is when the
// *token* was issued, not when the user was created. If you need the user's
// creation date, fetch it from the /me endpoint or include it in custom claims.
}
// Session data
interface Session {
accessToken: string;
refreshToken: string;
expiresAt: Date;
}
// Combined auth result
interface AuthResult {
user: User;
session: Session;
isNewUser?: boolean;
}
// Auth context (available in middleware)
interface AuthContext {
user: User | null;
session: Session | null;
isAuthenticated: boolean;
}License
MIT
