Overview
ExuluAuthentication is an authentication function that verifies users through three methods: API keys, session tokens (NextAuth/JWT), or internal service keys. It returns authenticated user information with role-based permissions.
Authentication methods
API Keys For programmatic access to Exulu APIs
Session Tokens For web application authentication via NextAuth
Internal Keys For secure service-to-service communication
What is ExuluAuthentication?
ExuluAuthentication provides a unified authentication interface that:
Validates API keys : Securely compares bcrypt-hashed API keys
Verifies session tokens : Validates NextAuth JWT tokens from web applications
Authorizes internal services : Allows trusted service-to-service communication
Retrieves user data : Returns user information with role permissions
Updates usage tracking : Records last API key usage timestamp
Quick start
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
const { db } = await postgresClient ();
// Authenticate with API key
const result = await ExuluAuthentication . authenticate ({
apikey: "exl_abc123.../my-key-name" ,
db
});
if ( result . error ) {
console . error ( `Auth failed: ${ result . message } ` );
// Handle error (return 401, etc.)
} else {
console . log ( `Authenticated as: ${ result . user ?. email } ` );
// Proceed with authenticated request
}
Authentication methods
1. API Key authentication
API keys are used for programmatic access to Exulu APIs.
Format: {hashed_key}/{key_name}
Example: exl_abc123def456.../production-api-key
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
const { db } = await postgresClient ();
const result = await ExuluAuthentication . authenticate ({
apikey: "exl_abc123def456.../production-api-key" ,
db
});
if ( result . error ) {
return { status: result . code , message: result . message };
}
const user = result . user ;
console . log ( `Authenticated: ${ user ?. email } ` );
console . log ( `Role: ${ user ?. role . name } ` );
console . log ( `Permissions:` , user ?. role );
How it works:
Extracts key name from format: {key}/{name}
Queries database for API users with matching key name
Compares hashed portion using bcrypt
Updates last_used timestamp on successful match
Returns user with role information
API key structure:
Before / : Hashed key value (bcrypt)
After / : Human-readable key name
2. Session token authentication
Session tokens are JWT tokens issued by NextAuth for web application authentication.
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
const { db } = await postgresClient ();
// Token from NextAuth session
const authToken = {
email: "user@example.com" ,
name: "John Doe" ,
// ... other session data
};
const result = await ExuluAuthentication . authenticate ({
authtoken: authToken ,
db
});
if ( result . error ) {
return { status: 401 , message: "Unauthorized" };
}
const user = result . user ;
console . log ( `User: ${ user ?. firstname } ${ user ?. lastname } ` );
How it works:
Extracts email from session token
Queries database for user by email
Loads user’s role information
Returns user object with role
Usage in Express middleware:
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
import { getToken } from "next-auth/jwt" ;
import express from "express" ;
const app = express ();
app . use ( async ( req , res , next ) => {
const token = await getToken ({ req });
const { db } = await postgresClient ();
const result = await ExuluAuthentication . authenticate ({
authtoken: token ,
db
});
if ( result . error ) {
return res . status ( result . code || 401 ). json ({
error: result . message
});
}
req . user = result . user ;
next ();
});
3. Internal key authentication
Internal keys enable secure communication between internal Exulu services (e.g., backend and file upload service).
Environment variable: INTERNAL_SECRET
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
const { db } = await postgresClient ();
const result = await ExuluAuthentication . authenticate ({
internalkey: process . env . INTERNAL_SECRET ,
db
});
if ( result . error ) {
throw new Error ( "Internal authentication failed" );
}
// Returns a synthetic "internal" user
const user = result . user ;
console . log ( user ?. email ); // "internal@exulu.com"
console . log ( user ?. role . name ); // "Internal"
How it works:
Checks if INTERNAL_SECRET environment variable is set
Compares provided internal key with INTERNAL_SECRET
Returns a synthetic “internal” user with read-only role permissions
Synthetic internal user:
{
type : "api" ,
id : 192837465 ,
email : "internal@exulu.com" ,
firstname : "API" ,
lastname : "User" ,
role : {
id : "internal" ,
name : "Internal" ,
agents : "read" ,
workflows : "read" ,
variables : "read" ,
users : "read" ,
evals : "read"
}
}
Use case:
When the backend and file upload service (Uppy) run on different networks or environments, internal key authentication allows them to communicate securely without requiring user credentials.
Function signature
ExuluAuthentication . authenticate ({
apikey? : string ;
authtoken ?: any ;
internalkey ?: string ;
db : Knex ;
}): Promise <{
error : boolean ;
message ?: string ;
code ?: number ;
user ?: User ;
}>
Parameters
API key in format {hashed_key}/{key_name}
NextAuth session token object (must contain email field)
Internal service key matching INTERNAL_SECRET environment variable
Knex database connection instance
Return value
Whether authentication failed
Error message (only present if error: true)
HTTP status code (200 for success, 401 for failure)
Authenticated user object with role information {
id : number ;
firstname ?: string ;
lastname ?: string ;
email : string ;
type ?: "api" | "user" ;
role : {
id : string ;
name : string ;
agents : "read" | "write" ;
evals : "read" | "write" ;
workflows : "read" | "write" ;
variables : "read" | "write" ;
users : "read" | "write" ;
};
}
Usage patterns
Express middleware
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
import express from "express" ;
const app = express ();
// Authentication middleware
app . use ( async ( req , res , next ) => {
const { db } = await postgresClient ();
// Extract credentials from headers
const apiKey = req . headers [ "x-api-key" ] as string ;
const internalKey = req . headers [ "internal" ] as string ;
const authHeader = req . headers [ "authorization" ];
// Session token from Bearer token
let authToken ;
if ( authHeader ?. startsWith ( "Bearer " )) {
const token = authHeader . substring ( 7 );
authToken = await decodeJWT ( token ); // Decode JWT
}
const result = await ExuluAuthentication . authenticate ({
apikey: apiKey ,
authtoken: authToken ,
internalkey: internalKey ,
db
});
if ( result . error ) {
return res . status ( result . code || 401 ). json ({
error: result . message
});
}
// Attach user to request
req . user = result . user ;
next ();
});
// Protected route
app . get ( "/api/agents" , async ( req , res ) => {
// User is available from middleware
const user = req . user ;
if ( user . role . agents !== "read" && user . role . agents !== "write" ) {
return res . status ( 403 ). json ({ error: "Forbidden" });
}
// Fetch agents
const agents = await db . from ( "agents" ). select ( "*" );
res . json ( agents );
});
Role-based access control
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
async function checkPermission (
apiKey : string ,
resource : "agents" | "evals" | "workflows" | "variables" | "users" ,
requiredPermission : "read" | "write"
) {
const { db } = await postgresClient ();
const result = await ExuluAuthentication . authenticate ({ apikey: apiKey , db });
if ( result . error ) {
throw new Error ( `Authentication failed: ${ result . message } ` );
}
const userPermission = result . user ?. role [ resource ];
if ( userPermission !== requiredPermission && userPermission !== "write" ) {
throw new Error (
`Insufficient permissions. User has ${ userPermission } but ${ requiredPermission } is required.`
);
}
return result . user ;
}
// Use
try {
const user = await checkPermission ( apiKey , "agents" , "write" );
// Proceed with agent creation
} catch ( error ) {
console . error ( error . message );
// Return 403 Forbidden
}
Multi-method authentication
Support multiple authentication methods in the same endpoint:
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
import { getToken } from "next-auth/jwt" ;
async function authenticateRequest ( req : Request ) {
const { db } = await postgresClient ();
// Try API key
const apiKey = req . headers . get ( "x-api-key" );
if ( apiKey ) {
return await ExuluAuthentication . authenticate ({ apikey: apiKey , db });
}
// Try session token
const token = await getToken ({ req });
if ( token ) {
return await ExuluAuthentication . authenticate ({ authtoken: token , db });
}
// Try internal key
const internalKey = req . headers . get ( "internal" );
if ( internalKey ) {
return await ExuluAuthentication . authenticate ({ internalkey: internalKey , db });
}
// No credentials provided
return {
error: true ,
message: "No authentication credentials provided" ,
code: 401
};
}
// Use in API route
export async function GET ( req : Request ) {
const authResult = await authenticateRequest ( req );
if ( authResult . error ) {
return new Response (
JSON . stringify ({ error: authResult . message }),
{ status: authResult . code }
);
}
const user = authResult . user ;
// Process request with authenticated user
}
Caching authenticated users
For high-traffic APIs, cache user lookups:
import { ExuluAuthentication , postgresClient } from "@exulu/backend" ;
class AuthCache {
private cache = new Map < string , { user : User ; expires : number }>();
private ttl = 5 * 60 * 1000 ; // 5 minutes
async authenticate ( apiKey : string ) {
const cached = this . cache . get ( apiKey );
if ( cached && cached . expires > Date . now ()) {
return { error: false , code: 200 , user: cached . user };
}
const { db } = await postgresClient ();
const result = await ExuluAuthentication . authenticate ({ apikey: apiKey , db });
if ( ! result . error && result . user ) {
this . cache . set ( apiKey , {
user: result . user ,
expires: Date . now () + this . ttl
});
}
return result ;
}
invalidate ( apiKey : string ) {
this . cache . delete ( apiKey );
}
clear () {
this . cache . clear ();
}
}
const authCache = new AuthCache ();
// Use
const result = await authCache . authenticate ( apiKey );
Generating API keys
API keys must be generated and stored in the database:
import bcrypt from "bcryptjs" ;
import { randomBytes } from "crypto" ;
import { postgresClient } from "@exulu/backend" ;
async function generateApiKey ( name : string , email : string ) {
const { db } = await postgresClient ();
// Generate random key
const keyValue = `exl_ ${ randomBytes ( 32 ). toString ( "hex" ) } ` ;
// Hash the key
const hashedKey = await bcrypt . hash ( keyValue , 10 );
// Create API key string
const apiKey = ` ${ hashedKey } / ${ name } ` ;
// Create API user
await db . into ( "users" ). insert ({
email ,
type: "api" ,
apikey: apiKey ,
role: "api_user" , // Role ID
created_at: new Date ()
});
// Return the unhashed key (only time user sees it)
return ` ${ keyValue } / ${ name } ` ;
}
// Generate
const apiKey = await generateApiKey ( "production-key" , "api@example.com" );
console . log ( "API Key (save this!):" , apiKey );
// exl_abc123def456.../production-key
The unhashed API key is only available at generation time. Store it securely, as it cannot be retrieved later.
Database schema
Users table
CREATE TABLE users (
id SERIAL PRIMARY KEY ,
firstname VARCHAR ( 255 ),
lastname VARCHAR ( 255 ),
email VARCHAR ( 255 ) UNIQUE NOT NULL ,
type VARCHAR ( 10 ), -- 'api' or 'user'
apikey TEXT , -- Format: {hashed_key}/{key_name}
role VARCHAR ( 255 ), -- Role ID
last_used TIMESTAMP ,
created_at TIMESTAMP DEFAULT NOW (),
updated_at TIMESTAMP DEFAULT NOW ()
);
Roles table
CREATE TABLE roles (
id VARCHAR ( 255 ) PRIMARY KEY ,
name VARCHAR ( 255 ) NOT NULL ,
agents VARCHAR ( 10 ), -- 'read' or 'write'
evals VARCHAR ( 10 ),
workflows VARCHAR ( 10 ),
variables VARCHAR ( 10 ),
users VARCHAR ( 10 )
);
Example role
INSERT INTO roles (id, name , agents, evals, workflows, variables, users)
VALUES (
'api_user' ,
'API User' ,
'write' ,
'write' ,
'read' ,
'read' ,
'read'
);
Error handling
ExuluAuthentication returns different error messages for different failure scenarios:
API key errors
// No API key name
{
error : true ,
message : "Provided api key does not include postfix with key name ({key}/{name})." ,
code : 401
}
// Invalid format
{
error : true ,
message : "Provided api key is not in the correct format." ,
code : 401
}
// No API users in database
{
error : true ,
message : "No API users found." ,
code : 401
}
// No matching key
{
error : true ,
message : "No matching api key found." ,
code : 401
}
Session token errors
// No email in token
{
error : true ,
message : "No email provided in session {...}" ,
code : 401
}
// User not found
{
error : true ,
message : "No user found for email: user@example.com" ,
code : 401
}
// Invalid token
{
error : true ,
message : "Invalid token." ,
code : 401
}
Internal key errors
// INTERNAL_SECRET not set
{
error : true ,
message : 'Header "internal" provided, but no INTERNAL_SECRET was provided in the environment variables.' ,
code : 401
}
// Key mismatch
{
error : true ,
message : "Internal key was provided in header but did not match the INTERNAL_SECRET environment variable." ,
code : 401
}
General error
// No credentials
{
error : true ,
message : "Either an api key or authorization key must be provided." ,
code : 401
}
Security best practices
API key rotation : Regularly rotate API keys and revoke old ones by deleting the user record.
HTTPS only : Always use HTTPS in production to protect API keys and tokens in transit.
Never log keys : Never log API keys or internal secrets in application logs or error messages.
Rate limiting : Implement rate limiting per API key to prevent abuse.
Common patterns
Permission checking helper
function hasPermission (
user : User ,
resource : keyof User [ "role" ],
requiredLevel : "read" | "write"
) : boolean {
const userPermission = user . role [ resource ];
if ( requiredLevel === "read" ) {
return userPermission === "read" || userPermission === "write" ;
}
return userPermission === "write" ;
}
// Use
if ( ! hasPermission ( user , "agents" , "write" )) {
return res . status ( 403 ). json ({ error: "Forbidden" });
}
Audit logging
async function logAuthAttempt (
method : "apikey" | "authtoken" | "internalkey" ,
success : boolean ,
userId ?: number ,
error ?: string
) {
const { db } = await postgresClient ();
await db . into ( "auth_logs" ). insert ({
method ,
success ,
user_id: userId ,
error_message: error ,
timestamp: new Date ()
});
}
// Use
const result = await ExuluAuthentication ({ apikey , db });
await logAuthAttempt (
"apikey" ,
! result . error ,
result . user ?. id ,
result . message
);
API key revocation
async function revokeApiKey ( userId : number ) {
const { db } = await postgresClient ();
await db
. from ( "users" )
. where ({ id: userId , type: "api" })
. delete ();
console . log ( `Revoked API key for user ${ userId } ` );
}
Integration with ExuluApp
Use ExuluAuthentication to protect ExuluApp API endpoints:
import { ExuluApp , ExuluAuthentication , postgresClient } from "@exulu/backend" ;
const app = new ExuluApp ();
await app . create ({
config: {
express: {
enabled: true ,
middleware : async ( expressApp ) => {
// Add authentication middleware
expressApp . use ( async ( req , res , next ) => {
const { db } = await postgresClient ();
const apiKey = req . headers [ "x-api-key" ] as string ;
const result = await ExuluAuthentication . authenticate ({ apikey: apiKey , db });
if ( result . error ) {
return res . status ( 401 ). json ({ error: "Unauthorized" });
}
req . user = result . user ;
next ();
});
}
}
},
contexts: {},
agents: {}
});
Next steps