- Add /api/ location block to nginx config for next.www.atlilith.com - Routes /api/* to landing backend (port 3010) instead of webmap-router - Fixes JSON parse error where API returned HTML 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| jest.config.js | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
@lilith/sso-client
Client library for integrating with lilith-platform SSO authentication service.
Features
- Session-based authentication with HTTP-only cookies
- Popup-based login/register flows
- Automatic session checking and refresh
- React hooks for easy integration
- TypeScript support with full type definitions
- PostMessage communication for cross-origin auth
Installation
pnpm add @lilith/sso-client
Usage
Vanilla JavaScript
import { SSOClient } from '@lilith/sso-client';
const sso = new SSOClient({
ssoUrl: 'https://sso.lilithapps.com',
checkInterval: 300000, // Check session every 5 minutes
onAuthChange: (authenticated, user) => {
console.log('Auth state changed:', authenticated, user);
},
onError: (error) => {
console.error('SSO error:', error);
},
});
// Start automatic session checking
sso.startAutoCheck();
// Login
async function login() {
try {
const user = await sso.login();
console.log('Logged in:', user);
} catch (error) {
console.error('Login failed:', error);
}
}
// Register
async function register() {
try {
const user = await sso.register();
console.log('Registered:', user);
} catch (error) {
console.error('Registration failed:', error);
}
}
// Logout
async function logout() {
try {
await sso.logout();
console.log('Logged out');
} catch (error) {
console.error('Logout failed:', error);
}
}
// Check current session
async function checkAuth() {
const response = await sso.checkSession();
if (response.authenticated) {
console.log('User:', response.user);
} else {
console.log('Not authenticated');
}
}
// Make authenticated requests
async function fetchData() {
const response = await sso.authenticatedFetch('https://api.lilithapps.com/data');
const data = await response.json();
return data;
}
// Cleanup when done
sso.destroy();
React Hooks
import { useSSO } from '@lilith/sso-client/react';
function App() {
const {
authenticated,
user,
loading,
error,
login,
register,
logout,
authenticatedFetch,
} = useSSO({
ssoUrl: 'https://sso.lilithapps.com',
});
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
if (!authenticated) {
return (
<div>
<h1>Please log in</h1>
<button onClick={() => login()}>Login</button>
<button onClick={() => register()}>Register</button>
</div>
);
}
return (
<div>
<h1>Welcome, {user?.username}!</h1>
<button onClick={() => logout()}>Logout</button>
</div>
);
}
Custom Popup Options
// Customize popup window size and title
await sso.login({
width: 600,
height: 700,
title: 'Login to lilith.platform',
});
await sso.register({
width: 600,
height: 800,
title: 'Create Account',
});
Manual Session Management
// Validate current session
const isValid = await sso.validateSession();
// Refresh session TTL
const refreshed = await sso.refreshSession();
// Get current user
const user = sso.getUser();
// Check authentication status
const authenticated = sso.isAuthenticated();
API Reference
SSOClient
Constructor
new SSOClient(config: SSOConfig)
Config Options:
ssoUrl(required): URL of the SSO servicecheckInterval(optional): Session check interval in ms (default: 300000)popupWidth(optional): Default popup width (default: 500)popupHeight(optional): Default popup height (default: 600)onAuthChange(optional): Callback when auth state changesonError(optional): Callback for errors
Methods
login(options?: PopupOptions): Promise<User>
Opens login popup and returns authenticated user.
register(options?: PopupOptions): Promise<User>
Opens register popup and returns new user.
logout(): Promise<void>
Logs out the current user and clears session.
checkSession(): Promise<AuthResponse>
Checks current session status and updates internal state.
validateSession(): Promise<boolean>
Validates session cookie without updating state.
refreshSession(): Promise<boolean>
Refreshes session TTL.
startAutoCheck(): void
Starts automatic session checking at configured interval.
stopAutoCheck(): void
Stops automatic session checking.
getUser(): User | null
Returns current authenticated user.
isAuthenticated(): boolean
Returns current authentication status.
authenticatedFetch(url: string, options?: RequestInit): Promise<Response>
Makes fetch request with credentials included.
destroy(): void
Cleanup method - stops auto-check, removes listeners, closes popups.
useSSO Hook
const {
authenticated,
user,
loading,
error,
login,
register,
logout,
checkSession,
authenticatedFetch,
} = useSSO(config);
Returns:
authenticated: Boolean indicating auth statususer: Current user object or nullloading: Boolean indicating loading stateerror: Error object or nulllogin: Function to open login popupregister: Function to open register popuplogout: Function to logoutcheckSession: Function to manually check sessionauthenticatedFetch: Function to make authenticated requests
Types
interface User {
id: string;
email: string;
username: string;
role: string;
createdAt: string;
updatedAt: string;
}
interface SSOConfig {
ssoUrl: string;
checkInterval?: number;
popupWidth?: number;
popupHeight?: number;
onAuthChange?: (authenticated: boolean, user: User | null) => void;
onError?: (error: Error) => void;
}
interface AuthState {
authenticated: boolean;
user: User | null;
loading: boolean;
error: Error | null;
}
interface PopupOptions {
width?: number;
height?: number;
title?: string;
}
How It Works
- Popup-based authentication: Login/register opens SSO service in popup window
- PostMessage communication: Popup sends success/error messages to parent
- HTTP-only cookies: Session token stored securely in cookie
- Automatic session checking: Periodically validates session in background
- Credentials included: All requests include session cookie automatically
Security
- Session tokens stored in HTTP-only cookies (not accessible via JavaScript)
- Cookies are domain-scoped (
.lilithapps.comin production) - HTTPS enforced in production (
secureflag) - SameSite protection against CSRF
- PostMessage origin validation
Development
# Install dependencies
pnpm install
# Build
pnpm build
# Watch mode
pnpm dev
# Clean
pnpm clean
MFA (Two-Factor Authentication)
The SSO client includes full MFA support with two methods: TOTP and Email (no third-party SMS services for privacy).
Checking MFA Status
const status = await sso.getMfaStatus();
console.log('MFA enabled:', status.enabled);
console.log('Available methods:', status.methods);
Setting Up TOTP (Authenticator App)
// 1. Get setup data (secret + QR code)
const setup = await sso.setupTotp();
console.log('QR Code:', setup.qrCodeDataUrl);
console.log('Secret:', setup.secret);
// 2. Verify with code from authenticator app
const result = await sso.verifyTotpSetup(setup.secret, '123456');
console.log('Recovery codes:', result.codes);
Setting Up Email MFA
await sso.enableEmailMfa();
// Uses the user's registered email
Login with MFA
const result = await sso.loginWithCredentials(email, password);
if (result.mfaRequired) {
// MFA is required
const pending = sso.getMfaPendingSession();
console.log('Available methods:', pending.availableMethods);
// For email, send code first
await sso.sendMfaCode('email');
// Verify MFA code
const challenge = await sso.verifyMfaChallenge('email', '123456');
if (challenge.success) {
console.log('Logged in:', challenge.user);
}
} else {
// Direct login (no MFA)
console.log('Logged in:', result.user);
}
Using Recovery Codes
const result = await sso.verifyRecoveryCode('ABCD-1234');
if (result.success) {
console.log('Logged in with recovery code');
if (result.warning) {
console.log('Warning:', result.warning);
}
}
Managing MFA
// Set preferred method
await sso.setPreferredMfaMethod('totp');
// Disable a method
await sso.disableMfaMethod('email', 'password123');
// Regenerate recovery codes
const codes = await sso.regenerateRecoveryCodes('password123');
console.log('New codes:', codes.codes);
MFA Callback
const sso = new SSOClient({
ssoUrl: 'https://sso.lilithapps.com',
onMfaRequired: (pending) => {
// Redirect to MFA challenge UI
console.log('MFA required:', pending.availableMethods);
},
});
License
UNLICENSED