# How FreeLLMAPI Authenticates Client Applications: Two-Key Model Explained

> Learn how FreeLLMAPI secures client applications with its two-key authentication model. Understand unified and per-profile keys, constant-time comparison, and SHA-256 hashing.

- Repository: [Tashfeen/freellmapi](https://github.com/tashfeenahmed/freellmapi)
- Tags: how-to-guide
- Published: 2026-08-28

---

**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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/db/index.ts) — the `getUnifiedApiKey()` function retrieves the plaintext value from the `settings` table under key `unified_api_key`
- **Security model**: Full inference access, but **no automatic system prompt injection**
- **Request format**: Use either `Authorization: Bearer <key>` or `x-api-key: <key>` header

The unified key validation uses **constant-time string comparison** to prevent timing attacks:

```typescript
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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/system-prompt.ts) handles both credential types:

```typescript
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

```bash
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)

```bash
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)

```bash
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**:

```json
{
  "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

```bash
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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/system-prompt.ts) | Core `resolveAuth` logic, `hashClientProfileKey()`, prefix constants |
| [`server/src/db/index.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/db/index.ts) | `getUnifiedApiKey()` database accessor |
| [`server/src/routes/client-profiles.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/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.