# Authentication Flows for OAuth, API Key, and Device-Code in Pi-Web

> Explore pi-web authentication flows for OAuth, API key, and device-code. Learn how SSE and REST endpoints manage secure AI provider access using the ModelRuntime class.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: deep-dive
- Published: 2026-08-13

---

**Pi-Web authenticates against AI providers using a dual-mode system where OAuth 2.0 (including device-code and manual-code variants) runs over server-sent events (SSE), while API keys are managed through stateless REST endpoints, both leveraging the `ModelRuntime` class from the `@earendil-works/pi-coding-agent` SDK.**

The `agegr/pi-web` repository provides a unified authentication layer for multiple AI providers, abstracting OAuth complexities and API key management behind HTTP endpoints. By utilizing the `ModelRuntime` class to detect provider capabilities, the system supports interactive browser-based logins, device-code flows for headless environments, and programmatic API key storage.

## OAuth Flow with Device-Code and Manual-Code Support

The OAuth implementation in `app/api/auth/login/[provider]/route.ts` uses a server-sent events (SSE) stream to manage interactive authentication sessions. This approach handles standard browser redirects, device-code flows for CLI environments, and manual token entry through a single unified interface.

### Initiating the SSE Stream

When a client opens `GET /api/auth/login/[provider]`, the server creates a fresh `ModelRuntime` instance and invokes `login(provider, "oauth", …)`. The runtime emits a sequence of `AuthPrompt` objects that drive the authentication process.

The SSE stream supports four primary event types emitted via the `send(controller, …)` utility (lines 124-140):

- **`select_request`** – Requires the user to choose between options (e.g., "api-key" vs "bearer-token")
- **`prompt_request`** – Requests free-form input such as secrets or verification codes  
- **`auth`** – Provides an OAuth URL for browser-based authorization
- **`device_code`** – Triggers the device-code flow with user codes and verification URIs

### Device-Code Flow Implementation

For providers supporting device-code authorization, the runtime emits a `device_code` event containing `userCode`, `verificationUri`, polling interval, and expiration details (lines 151-158). The client displays these credentials to the user while the server automatically handles polling until authorization completes.

### Manual-Code and Token Resolution

When providers require manual verification codes after redirects, the runtime emits a `prompt_request` with a unique token generated via `getManualInputRequest()` (lines 95-105). The client later POSTs the response to `/api/auth/login/[provider]` with the payload `{ token, code }`.

The server maintains a `__piLoginCallbacks` map to resolve pending promises. The token format follows `<provider>-<timestamp>-<random>` (line 74), ensuring provider-scoped callbacks. Upon resolution, the server invokes `cleanup()` (line 107) to remove pending tokens and calls `invalidateModelsCache()` to refresh the available models (lines 165-176).

## API Key Authentication Flow

The API key implementation in `app/api/auth/api-key/[provider]/route.ts` provides a non-interactive alternative to OAuth, using standard REST semantics for credential management.

### Checking Provider Configuration

The `GET` handler returns a compact status object containing `configured`, `source`, and `models` without exposing the actual secret (lines 10-18). This allows the UI to display authentication status safely.

### Storing and Validating Keys

The `POST` handler accepts `{ apiKey }` and validates that the key is a non-empty string (lines 25-27). The server then creates a `ModelRuntime` and invokes `provider.auth.apiKey.login()` using a two-step prompt sequence: first a *select* prompt to choose the "api-key" option (lines 38-42), followed by a *secret* prompt supplying the key (lines 43-46).

Upon successful authentication, the credential persists via `storeProviderCredential()` (lines 51-53), and the models cache invalidates via `invalidateModelsCache()` (line 54).

### Removing Stored Credentials

The `DELETE` handler removes API keys via `removeStoredCredentialIfType`. If the provider currently uses OAuth, the endpoint returns a 409 Conflict error (lines 61-73).

## Client Implementation Examples

**OAuth Client (TypeScript):**

```typescript
// Open SSE stream for a provider, e.g. "anthropic"
const evts = new EventSource(`/api/auth/login/anthropic`);

evts.addEventListener('select_request', e => {
  const { options, token } = JSON.parse(e.data);
  // Auto-select "api-key" option
  fetch('/api/auth/login/anthropic', {
    method: 'POST',
    body: JSON.stringify({ token, code: 'api-key' })
  });
});

evts.addEventListener('device_code', e => {
  const { userCode, verificationUri } = JSON.parse(e.data);
  // Display QR/code to user; polling happens automatically
});

evts.addEventListener('auth', e => {
  const { url, token } = JSON.parse(e.data);
  // Open URL in new tab; provider POSTs verification code back to server
});

```

**API Key Client (TypeScript):**

```typescript
async function setApiKey(provider: string, key: string) {
  const resp = await fetch(`/api/auth/api-key/${provider}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ apiKey: key })
  });
  const result = await resp.json();
  if (!result.success) throw new Error(result.error);
}

// Delete API key
await fetch(`/api/auth/api-key/${provider}`, { method: 'DELETE' });

```

## Key Source Files

| File | Purpose |
|------|---------|
| `app/api/auth/login/[provider]/route.ts` | OAuth SSE stream, device-code handling, and token resolution (lines 17-42, 44-66, 151-158) |
| `app/api/auth/api-key/[provider]/route.ts` | API key GET/POST/DELETE operations (lines 10-18, 20-50, 61-73) |
| [`lib/provider-credential-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/provider-credential-store.ts) | Safe read/write operations for [`auth.json`](https://github.com/agegr/pi-web/blob/main/auth.json) and credential removal |
| [`lib/models-cache.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-cache.ts) | Central model cache with `invalidateModelsCache()` function |
| [`lib/web-auth.ts`](https://github.com/agegr/pi-web/blob/main/lib/web-auth.ts) | Basic-auth password protection utilities for the proxy layer |

## Summary

- Pi-Web uses **server-sent events (SSE)** for interactive OAuth flows, supporting browser redirects, device-code, and manual-code authentication through `GET /api/auth/login/[provider]`.
- The **token-based callback system** uses the format `<provider>-<timestamp>-<random>` to securely match client responses with pending server-side promises via `POST /api/auth/login/[provider]`.
- **Device-code authentication** emits specific SSE events containing `userCode` and `verificationUri`, enabling headless CLI authentication without browser interaction.
- **API key management** operates over stateless REST endpoints (`GET`, `POST`, `DELETE`) at `/api/auth/api-key/[provider]`, automatically invalidating the models cache upon credential changes.
- All authentication paths leverage the **`ModelRuntime`** class from the `@earendil-works/pi-coding-agent` SDK to handle provider-specific logic and credential persistence.

## Frequently Asked Questions

### How does Pi-Web handle OAuth callbacks without a traditional redirect URI?

The OAuth flow uses a **token polling mechanism** rather than direct browser redirects to the client. When the user completes authorization in their browser, the provider sends the verification code to the Pi-Web server via the `POST /api/auth/login/[provider]` endpoint. The server matches this code to the pending SSE session using a unique token identifier, resolving the authentication promise and completing the flow without requiring the client to host a callback server.

### What is the difference between device-code and manual-code authentication in Pi-Web?

**Device-code** authentication (lines 151-158) is designed for devices that cannot easily open a browser, emitting `userCode` and `verificationUri` via SSE for the user to enter on a separate device. **Manual-code** authentication handles providers that return a verification code after a browser redirect, using the `prompt_request` SSE event to request that the user paste the code back into the application. Both methods ultimately POST the resulting code to the same token resolution endpoint.

### Can API keys and OAuth credentials coexist for the same provider?

No, the system enforces mutual exclusivity. The `DELETE` handler for API keys (lines 61-73 in `app/api/auth/api-key/[provider]/route.ts`) returns a **409 Conflict** if the provider is currently configured using OAuth. Similarly, storing an API key via `storeProviderCredential()` replaces any existing OAuth credentials for that provider, ensuring only one authentication method is active per provider at any time.

### How does the server clean up abandoned OAuth login attempts?

The server registers an `abort.signal` listener that automatically invokes `cleanup()` (line 107) when the client disconnects or the request terminates. This removes the pending token from `__piLoginCallbacks`, preventing memory leaks from abandoned authentication sessions. Additionally, successful or failed logins trigger immediate cleanup of their respective promise entries.