# Authentication System Design **Component:** Authentication System (Rust frontend + TypeScript backend) **Status:** Implemented **Related:** [Backend Adapter](./backend-adapter.md), [Persistence System](./persistence-system.md) --- ## Overview Tiny Workshop uses z.AI as the API provider for Claude access. Authentication is simple: users provide their z.AI API key, which is stored securely in the OS keychain. ### Design Goals 1. **Simple**: Just an API key - no OAuth flows or token management 2. **Secure**: Use OS keychain for credential storage, never log credentials 3. **Persistent**: API key survives app restarts 4. **Graceful errors**: Handle invalid/missing keys with clear user feedback --- ## Architecture ``` ┌──────────────────────────────────────────────────────────────┐ │ Frontend (Rust) │ ├──────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ CredentialManager │ │ │ │ │ │ │ │ - Check for stored API key in keychain │ │ │ │ - Prompt user for key if not found │ │ │ │ - Store API key after user input │ │ │ │ - Pass to backend via IPC init message │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ │ IPC (api_key in init message) ▼ ┌──────────────────────────────────────────────────────────────┐ │ Backend Adapter (Bun) │ ├──────────────────────────────────────────────────────────────┤ │ │ │ Sets environment variables: │ │ - ANTHROPIC_AUTH_TOKEN = api_key │ │ - ANTHROPIC_BASE_URL = https://api.z.ai/api/anthropic │ │ │ │ Passes to Claude Agent SDK │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ SDK requests ▼ https://api.z.ai/api/anthropic ``` --- ## User Flow ### First Launch (No API Key) 1. App starts, checks keychain for stored API key 2. No key found → shows system message: "Please enter your z.AI API key" 3. User enters API key in chat input 4. Frontend stores key in keychain 5. Frontend sends `init` message to backend with API key 6. Backend sets environment variables and starts SDK 7. If successful: normal operation begins 8. If auth fails: backend sends `auth_status` with `authenticated: false` and error message ### Subsequent Launches 1. App starts, loads API key from keychain 2. Frontend sends `init` message with stored API key 3. Backend authenticates with z.AI 4. Normal operation begins ### Invalid API Key 1. Backend receives `init` with API key 2. SDK fails authentication with z.AI 3. Backend sends `auth_status` with `authenticated: false` and error message 4. Frontend shows error and prompts for new API key --- ## Credential Manager The `CredentialManager` in Rust handles API key storage: ```rust pub struct CredentialManager; impl CredentialManager { /// Store the z.AI API key. pub fn store_api_key(key: &str) -> Result<(), CredentialError>; /// Get the stored API key. pub fn get_api_key() -> Option; /// Clear the stored API key (logout). pub fn clear() -> Result<(), CredentialError>; /// Check if we have a stored API key. pub fn has_api_key() -> bool; /// Check if the OS keychain is available. pub fn keychain_available() -> bool; /// Get which storage backend is being used. pub fn storage_type() -> StorageType; } ``` ### Storage Constants ```rust const SERVICE_NAME: &str = "agent-study-api-key"; const API_KEY_KEY: &str = "zai_api_key"; const TOKEN_FILE_NAME: &str = "zai_api_key"; ``` --- ## Platform-Specific Keychain Behavior | Platform | Keychain | Access Permission | |----------|----------|-------------------| | **macOS** | macOS Keychain | User prompted on first access | | **Windows** | Windows Credential Manager | No prompt (per-user storage) | | **Linux** | Secret Service API (requires D-Bus) | Depends on keyring daemon (gnome-keyring, KWallet) | ### Linux Fallback When Secret Service is unavailable on Linux (no gnome-keyring, KWallet, etc.), the credential manager automatically falls back to file-based storage: **Storage Location:** `~/.agent-study/zai_api_key` **Security Measures:** - File permissions: 0600 (user-only read/write) - Atomic writes: temp file + rename pattern to prevent corruption - Warning logged at startup when using fallback ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StorageType { /// OS keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service) Keychain, /// Config file fallback (~/.agent-study/zai_api_key) ConfigFile, } ``` --- ## IPC Messages ### Frontend → Backend #### `init` ```typescript { type: 'init'; version: number; // Protocol version (currently 1) working_dir: string; // Absolute path to project directory api_key?: string; // z.AI API key (optional) resume_session_id?: string; // SDK session ID to resume (optional) } ``` ### Backend → Frontend #### `auth_status` ```typescript { type: 'auth_status'; authenticated: boolean; // Currently authenticated? error?: string; // Error message (only when authenticated=false) } ``` --- ## Backend Environment Variables The backend adapter sets these environment variables for the Claude Agent SDK: | Variable | Value | |----------|-------| | `ANTHROPIC_AUTH_TOKEN` | User's z.AI API key | | `ANTHROPIC_BASE_URL` | `https://api.z.ai/api/anthropic` | The SDK then uses these automatically for all API requests. --- ## Error Handling ```rust #[derive(Debug)] pub enum CredentialError { /// Error from the OS keychain Keychain(keyring::Error), /// Error from file I/O operations Io(std::io::Error), } ``` ### User-Facing Error Messages | Error | User Message | Action | |-------|--------------|--------| | No API key | "Please enter your z.AI API key" | Show input prompt | | Invalid API key | "Invalid API key" | Show input prompt again | | Keychain error | "Unable to access keychain: {details}" | Fallback to config file on Linux | --- ## Security Considerations 1. **Never log API keys**: Redact from logs, error messages, debug output 2. **Keychain encryption**: OS keychain encrypts data at rest 3. **Config file warning**: Linux fallback uses file permissions only (not encrypted) 4. **Memory handling**: API key kept in memory only as long as needed ### Credential Redaction ```rust impl Debug for ApiKey { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "ApiKey(***)") } } ``` --- ## Testing ### Unit Tests ```rust #[test] fn test_storage_type_detection() { let storage = CredentialManager::storage_type(); // Should return Keychain on macOS/Windows, either on Linux println!("Detected storage type: {storage:?}"); } ``` ### Manual Testing Checklist 1. **Fresh install**: App prompts for API key, stores in keychain 2. **Restart**: API key loaded from keychain automatically 3. **Invalid key**: Error shown, can retry with new key 4. **Clear and re-enter**: Can logout and enter new key 5. **Linux fallback**: When no Secret Service, uses file storage --- ## Related Documents - [Backend Adapter](./backend-adapter.md) - SDK integration - [IPC Protocol](./ipc-protocol.md) - Message schemas - [Persistence System](./persistence-system.md) - Storage patterns