WIP experiment with 95% vibe coding
tiny-workshop docs design authentication-system.md
9.2 kB
Markdown
at main

Authentication System Design #

Component: Authentication System (Rust frontend + TypeScript backend) Status: Implemented Related: Backend Adapter, Persistence System


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:

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<String>;

    /// 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 #

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
#[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 #

{
  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 #

{
  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 #

#[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 #

impl Debug for ApiKey {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "ApiKey(***)")
    }
}

Testing #

Unit Tests #

#[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