How the Pi-Web Auth API Handles OAuth Flows and API Key Storage

The pi-web auth API manages OAuth device-code flows and API key credentials through a centralized JSON store at ~/.pi/agent/auth.json, using type-aware helpers with file locking to prevent race conditions.

The agegr/pi-web repository implements a unified authentication layer that supports both OAuth device-code flows and static API key storage. This system persists credentials to a single JSON file with restrictive permissions while exposing REST endpoints for provider discovery, login, and secure logout.

Credential Storage Architecture

All authentication data lives in (~/.pi/agent/auth.json), a JSON file created with mode 0o600 (read/write for owner only). Each entry maps a provider ID to a credential object containing a type field—either "oauth" or "api_key"—followed by type-specific data such as deviceCode, accessToken, or key. This unified store allows the system to handle multiple authentication methods through a single, type-safe interface.

OAuth Device-Code Flow Implementation

The OAuth implementation uses Server-Sent Events (SSE) to stream progress updates during the device-code authorization flow.

Route: app/api/auth/login/[provider]/route.ts

When a client opens an SSE connection to GET /api/auth/login/[provider], the handler invokes the Pi SDK's loginOAuthProvider() method. This initiates the device-code flow and emits events (code, verification_uri, access_token) that the route forwards to the client. Upon successful completion, the SDK writes an OAuth credential object to auth.json:

{
  "type": "oauth",
  "deviceCode": "...",
  "accessToken": "..."
}

Client-side implementation:

const evtSource = new EventSource(`/api/auth/login/${providerId}`);
evtSource.onmessage = e => {
  const data = JSON.parse(e.data);
  if (data.type === 'device_code') {
    console.log('Enter this code:', data.code);
    console.log('Visit:', data.verification_uri);
  } else if (data.type === 'access_token') {
    console.log('Login successful!');
    evtSource.close();
  }
};

API Key Storage and Validation

For providers that do not support OAuth, pi-web accepts static API keys via POST /api/auth/api-key/[provider].

Route: app/api/auth/api-key/[provider]/route.ts

The handler validates that the provider is not an OAuth-only provider (returning HTTP 409 if it is), then writes a credential object to the store:

{
  "type": "api_key",
  "key": "sk-XXXXXXXXXXXXXXXX"
}

Example request:

await fetch(`/api/auth/api-key/${providerId}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ apiKey: 'sk-XXXXXXXXXXXXXXXX' })
});

Secure Credential Removal

The logout endpoint POST /api/auth/logout/[provider] handles deletion for both credential types through the removeStoredCredentialIfType() helper in lib/provider-credential-store.ts.

This utility performs the following steps:

  1. Acquires a file lock using proper-lockfile to prevent concurrent modifications.
  2. Verifies the stored credential's type matches the expected type ("oauth" or "api_key").
  3. Deletes the entry and rewrites auth.json with mode 0o600.

Example logout:

await fetch(`/api/auth/logout/${providerId}`, { method: 'POST' });

Provider Discovery

The frontend determines which authentication flow to present using GET /api/auth/providers.

Implementation: lib/provider-listing.ts

The buildOAuthProviderList() function queries the Pi SDK's model registry and filters for providers exposing an OAuth login method, returning an array of supported providers.

Example usage:

const providers = await fetch('/api/auth/providers')
  .then(r => r.json()); 
// → [{ id: "github", name: "GitHub" }, …]

Security Mechanisms

The authentication system implements several safeguards to protect credential data:

  • File permissions: All writes to auth.json use mode 0o600, ensuring only the owner can read or write credentials.
  • Race condition prevention: The proper-lockfile library locks the credential file during all read and write operations.
  • Type verification: The logout route explicitly checks the stored credential type before deletion, preventing accidental removal of OAuth tokens when an API key is expected, or vice versa.

Summary

  • The pi-web auth API uses a single JSON file (~/.pi/agent/auth.json) to store both OAuth and API key credentials, differentiated by a type field.
  • OAuth authentication follows the device-code flow via SSE endpoints that stream progress events to the client.
  • API keys are validated against provider capabilities before storage to prevent conflicts with OAuth-only providers.
  • The removeStoredCredentialIfType() helper in lib/provider-credential-store.ts ensures type-safe, locked deletion of credentials.
  • File permissions (mode 600) and proper-lockfile locking prevent unauthorized access and corruption.

Frequently Asked Questions

Where does pi-web store authentication credentials?

All credentials are stored in ~/.pi/agent/auth.json as a single JSON object. The file is created with restrictive permissions (mode 0o600) and contains entries for each provider with fields indicating whether the credential is an OAuth token or an API key.

How does the OAuth device-code flow work in pi-web?

The flow initiates via GET /api/auth/login/[provider], which opens an SSE connection. The server calls pi.loginOAuthProvider() to generate a device code, then streams events containing the verification URI and user code to the client. Once the user authorizes the device, the SDK writes the access token to auth.json and sends a completion event to close the stream.

Can I store an API key for a provider that also supports OAuth?

No. The API key endpoint (POST /api/auth/api-key/[provider]) explicitly checks if the provider supports OAuth and returns HTTP 409 Conflict if it does. This prevents credential conflicts and ensures providers use their intended authentication method according to the Pi SDK's provider registry.

How does pi-web prevent race conditions when updating credentials?

All credential mutations use the proper-lockfile library to acquire exclusive file locks on auth.json before reading or writing. Additionally, the removeStoredCredentialIfType() function verifies the credential type matches expectations before deletion, preventing accidental overwrites during concurrent operations.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →