Authentication
The Admin UI supports JWT-based authentication with customizable login and token validation.
Overview
Authentication flow:
- User submits email/password on login page
loginfunction validates credentials- JWT token is generated and stored in cookie
principalForTokenvalidates token on each request- Principal object provides permissions
Configuration
| Property | Type | Description |
|---|---|---|
| login | Function | Validates credentials and returns token |
| principalForToken | Function | Validates token and returns Principal |
login
login?: (
runtime: ActiveQLServer,
props: { email: string; password: string }
) => Promise<{ token?: string }>| Type | Default | Description |
|---|---|---|
Function | - | Custom login handler |
Return an object with token on successful authentication, or empty object on failure.
Example
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
{
login: async (runtime, { email, password }) => {
// Find user
const user = await runtime.entity('User').findOneByAttribute({ email });
if (!user) {
return {}; // Invalid email
}
// Verify password
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return {}; // Invalid password
}
// Check if user can access admin
if (!user.roles.includes('admin')) {
return {}; // Not authorized
}
// Generate JWT
const token = jwt.sign(
{
userId: user.id,
email: user.email,
roles: user.roles
},
process.env.JWT_SECRET,
{ expiresIn: '8h' }
);
return { token };
}
}principalForToken
principalForToken?: (
token: string,
runtime: ActiveQLServer
) => Promise<Principal | undefined>| Type | Default | Description |
|---|---|---|
Function | - | Token validation and Principal creation |
Called on every authenticated request. Return a Principal object or undefined if token is invalid.
Example
import jwt from 'jsonwebtoken';
import { Principal } from 'activeql-server';
{
principalForToken: async (token, runtime) => {
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Create Principal
return new Principal(
decoded.userId,
decoded.roles
);
} catch (error) {
// Token invalid or expired
return undefined;
}
}
}Principal Object
The Principal represents the authenticated user and controls permissions.
class Principal {
constructor(
public id: string | number,
public roles: string[]
) {}
}Permission Checks
Permissions are checked automatically for CRUD operations:
- READ: List and show items
- CREATE: Create new items
- UPDATE: Edit existing items
- DELETE: Delete items
Configure entity permissions in your ActiveQL schema.
Login Page Customization
Customize the login page content:
{
content: {
login: (activeAdmin) => ({
title: 'Admin Login',
subtitle: 'Please sign in to continue',
logo: '/img/logo.png'
})
}
}Session Management
- Token is stored in HTTP-only cookie
- Default expiration: 8 hours (configurable in JWT)
- Logout clears the cookie
Complete Example
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { Principal } from 'activeql-server';
import { addActiveAdmin } from 'activeql-active-admin';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
addActiveAdmin({
path: '/admin',
title: 'My Admin',
login: async (runtime, { email, password }) => {
// Find user by email
const user = await runtime.entity('User').findOneByAttribute({ email });
if (!user) {
console.log('Login failed: user not found', email);
return {};
}
// Verify password
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
console.log('Login failed: invalid password', email);
return {};
}
// Check admin role
if (!user.isAdmin && !user.roles?.includes('admin')) {
console.log('Login failed: not an admin', email);
return {};
}
// Generate token
const token = jwt.sign(
{
userId: user.id,
email: user.email,
roles: user.roles || (user.isAdmin ? ['admin'] : [])
},
JWT_SECRET,
{ expiresIn: '8h' }
);
// Log successful login
console.log('Login successful', email);
return { token };
},
principalForToken: async (token, runtime) => {
try {
const decoded = jwt.verify(token, JWT_SECRET);
// Optionally refresh user data
const user = await runtime.entity('User').findById(decoded.userId);
if (!user) {
return undefined; // User deleted
}
if (!user.isActive) {
return undefined; // User deactivated
}
return new Principal(
decoded.userId,
user.roles || decoded.roles
);
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.log('Token expired');
} else {
console.log('Token validation failed', error.message);
}
return undefined;
}
},
content: {
login: () => ({
title: 'Admin Panel',
subtitle: 'Enter your credentials'
})
},
entities: {
// Entity configurations
}
});Security Best Practices
- Use strong JWT secret: At least 256 bits of entropy
- Set appropriate expiration: Balance security and usability
- Use HTTPS: Always in production
- Hash passwords: Use bcrypt with appropriate cost factor
- Validate on every request: Always verify token freshness
- Check user status: Account might be disabled after login
- Log authentication events: For security auditing
- Rate limit login attempts: Prevent brute force attacks
Environment Variables
JWT_SECRET=your-very-long-secret-key-with-high-entropy
JWT_EXPIRATION=8hconst config = {
login: async (runtime, { email, password }) => {
// ...
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRATION || '8h'
});
return { token };
}
};