How FreeLLMAPI Authenticates Client Applications: Two-Key Model Explained
FreeLLMAPI authenticates requests using either a unified API key for general access or per-profile keys that enforce custom system prompts, with both token types validated through constant-time comparison and SHA-256 hash lookups.
FreeLLMAPI implements a dual-credential authentication system designed for flexible LLM inference access. Whether you need a single master key for internal tools or scoped keys that automatically inject system prompts for external clients, the authentication flow is handled by the resolveAuth function in server/src/lib/system-prompt.ts.
Unified API Key: Global Access Without System Prompt Enforcement
The unified API key serves as the server's master credential. It's generated once during initialization and stored in the database's settings table.
Key characteristics:
- Storage location:
server/src/db/index.ts— thegetUnifiedApiKey()function retrieves the plaintext value from thesettingstable under keyunified_api_key - Security model: Full inference access, but no automatic system prompt injection
- Request format: Use either
Authorization: Bearer <key>orx-api-key: <key>header
The unified key validation uses constant-time string comparison to prevent timing attacks:
if (timingSafeStringEqual(token, getUnifiedApiKey())) {
return { kind: 'unified', systemPrompt: null };
}
Client Profile Keys: Scoped Access with Enforced System Prompts
For multi-tenant scenarios, client profile keys provide isolated access with mandatory prompt injection.
How Profile Keys Work
| Aspect | Implementation |
|---|---|
| Prefix | sk-cp- (defined as CLIENT_PROFILE_KEY_PREFIX) |
| Generation | mintClientProfileKey() in server/src/routes/client-profiles.ts |
| Storage | SHA-256 hash in token_hash column; plaintext never persisted |
| Lookup | Hash comparison against client_profiles table |
| Enforcement | System prompt prepended to every inference request |
The Authentication Resolution Flow
The resolveAuth function in server/src/lib/system-prompt.ts handles both credential types:
export function resolveAuth(token: string | undefined): ResolvedAuth | null {
if (!token) return null;
// Check unified key first
if (timingSafeStringEqual(token, getUnifiedApiKey())) {
return { kind: 'unified', systemPrompt: null };
}
// Must be a client profile key
if (!token.startsWith(CLIENT_PROFILE_KEY_PREFIX)) return null;
const row = getDb()
.prepare('SELECT id, name, system_prompt, enabled FROM client_profiles WHERE token_hash = ?')
.get(hashClientProfileKey(token)) as ProfileRow | undefined;
if (!row || !row.enabled) return null; // Disabled = unknown (security through obscurity)
const prompt = row.system_prompt?.trim().length ? row.system_prompt : null;
return {
kind: 'profile',
profileId: row.id,
name: row.name,
systemPrompt: prompt
};
}
Security Design: Disabled Profiles Are Indistinguishable from Invalid Keys
Notice that !row || !row.enabled returns null in both cases. This prevents clients from fingerprinting whether a key was revoked versus never existed.
Practical Code Examples
Authenticate with the Unified Key
curl -X POST https://api.freellmapi.com/v1/chat/completions \
-H "Authorization: Bearer $(cat ~/.freellmapi/unified.key)" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{ "role": "user", "content": "Hello!" }]
}'
Authenticate with a Client Profile Key (System Prompt Enforced)
curl -X POST https://api.freellmapi.com/v1/chat/completions \
-H "x-api-key: sk-cp-0123abcd4567efgh8901ijkl" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{ "role": "user", "content": "Explain quantum tunneling." }]
}'
Create a New Client Profile (Dashboard Session Required)
curl -X POST https://api.freellmapi.com/api/client-profiles \
-H "Cookie: session=YOUR_DASHBOARD_SESSION" \
-H "Content-Type: application/json" \
-d '{"name":"MyBot","systemPrompt":"You are a helpful assistant that never discloses confidential info."}'
Response contains the plaintext key exactly once:
{
"id": 3,
"name": "MyBot",
"maskedKey": "[encrypted]",
"systemPrompt": "You are a helpful assistant ...",
"enabled": true,
"createdAt": "2024-10-01T12:34:56Z",
"updatedAt": "2024-10-01T12:34:56Z",
"key": "sk-cp-7f9e1c8d5b3a4e6f..."
}
Rotate a Compromised Profile Key
curl -X POST https://api.freellmapi.com/api/client-profiles/3/rotate \
-H "Cookie: session=YOUR_DASHBOARD_SESSION"
This invalidates the old hash and generates a new sk-cp- prefix key.
Key Source Files for FreeLLMAPI Authentication
| File | Responsibility |
|---|---|
server/src/lib/system-prompt.ts |
Core resolveAuth logic, hashClientProfileKey(), prefix constants |
server/src/db/index.ts |
getUnifiedApiKey() database accessor |
server/src/routes/client-profiles.ts |
Profile CRUD, key generation (mintClientProfileKey), encrypted display |
| Middleware (inference routes) | requireInferenceAuth wraps resolveAuth for all /v1/* endpoints |
Summary
- FreeLLMAPI authentication supports two credential types: a unified key for simple access and profile-scoped keys with prompt enforcement
- Constant-time comparison (
timingSafeStringEqual) prevents timing attacks against the unified key - SHA-256 hashing ensures plaintext profile keys never touch persistent storage — only hashes do
- System prompt injection happens server-side via
prependSystemPrompt, making override impossible - Status hiding: Disabled profiles return identical errors to non-existent keys, preventing information leakage
Frequently Asked Questions
What happens if I send a request without any authentication header?
The resolveAuth function receives undefined and returns null, causing the middleware to reject the request with a 401 Unauthorized response. FreeLLMAPI requires either Authorization: Bearer <token> or x-api-key: <token> on all inference endpoints.
Can I use the same header format for both unified and profile keys?
Yes. Both credential types accept Authorization: Bearer <token> or x-api-key: <token>. The server detects the key type by checking for the sk-cp- prefix after the unified key comparison fails.
How does FreeLLMAPI prevent clients from bypassing enforced system prompts?
The system prompt retrieved during resolveAuth is prepended to the message array in prependSystemPrompt before forwarding to the LLM provider. Since this happens server-side after authentication, the caller has no mechanism to inspect, modify, or remove the injected prompt.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →