# OAuth and API-Key Authentication Flow in pi-web: Complete Implementation Guide

> Master the OAuth and API-key authentication flow in pi-web with this comprehensive implementation guide. Securely manage AI provider credentials using SSE and direct submission.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-07-23

---

**The pi-web application implements dual authentication mechanisms—OAuth via Server-Sent Events (SSE) with token-bound callbacks and direct API-Key submission—both mediated through the `ModelRuntime` class from `@earendil-works/pi-coding-agent` to manage AI provider credentials securely.**

The `agegr/pi-web` repository provides a Next.js-based interface for connecting to external AI providers like OpenAI and Anthropic. To handle identity verification, the codebase implements a comprehensive **OAuth and API-key authentication flow** through specialized API routes that manage credential discovery, interactive login prompts, and secure storage.

## How OAuth Authentication Works in pi-web

The OAuth implementation uses a bidirectional communication pattern where the server pushes authentication prompts via Server-Sent Events (SSE) and receives user responses through token-bound POST requests.

### Discovering Available Providers

The endpoint `GET /api/auth/providers` defined in [`app/api/auth/providers/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/auth/providers/route.ts) returns a filtered list of providers that support OAuth authentication. The route instantiates `ModelRuntime` and queries existing credentials to determine login status.

```typescript
// app/api/auth/providers/route.ts
const modelRuntime = await ModelRuntime.create();
const credentials = await modelRuntime.listCredentials();
const loggedInProviders = new Set(
  credentials.filter(c => c.type === "oauth").map(c => c.providerId)
);
const providers = modelRuntime.getProviders().filter(p => p.auth.oauth);

```

This returns provider metadata including display names and authentication status, enabling the frontend to render appropriate login buttons.

### Initiating the SSE Login Stream

The dynamic route `app/api/auth/login/[provider]/route.ts` handles OAuth initialization through Server-Sent Events. When a client opens an SSE connection, the server generates a unique token using the pattern `` `${provider}-${Date.now()}-${Math.random()...` `` and registers a callback in the global `__piLoginCallbacks` registry.

Key implementation details:

- **Abort handling**: An `AbortController` links the SSE connection to the client request, terminating the login process if the client disconnects
- **Prompt callbacks**: The `modelRuntime.login()` method receives `prompt` and `notify` callbacks to handle interactive authentication steps
- **Event types**: The SSE stream emits `select_request`, `prompt_request`, `auth` (containing URLs), `device_code`, and `progress` events

### Handling Token-Bound Responses

When the user provides input (e.g., a verification code), the client sends a POST request to the same login endpoint with a JSON payload containing the token and response code.

```typescript
// app/api/auth/login/[provider]/route.ts
const { token, code } = await req.json();
const callbacks = registry.get(token);
if (!callbacks) {
  return Response.json({ error: "No pending login for token" }, { status: 404 });
}
callbacks.resolve(code);
registry.delete(token);

```

The registry lookup validates the token belongs to the specific provider, then resolves the pending promise in the SSE handler. Upon resolution, `ModelRuntime.login` completes the OAuth exchange, stores the credentials, and the SSE stream emits a final `{ type: "success" }` event before closing.

## API-Key Authentication Implementation

For providers supporting API-Key authentication, `pi-web` provides RESTful endpoints in `app/api/auth/api-key/[provider]/route.ts` to manage credentials without interactive flows.

### Checking Authentication Status

The `GET` method retrieves the current authentication state without exposing sensitive secrets:

```typescript
// app/api/auth/api-key/[provider]/route.ts
const status = modelRuntime.getProviderAuthStatus(provider);
return NextResponse.json({
  provider,
  displayName,
  configured: status.configured,
  source: status.source,
  models: modelRuntime.getModels(provider).length,
});

```

This returns a `configured` boolean indicating whether credentials exist, along with the count of available models for that provider.

### Submitting API Keys

The `POST` handler accepts `{ apiKey: "..." }` and orchestrates validation through `ModelRuntime`. The implementation handles provider-specific authentication method selection automatically:

```typescript
// app/api/auth/api-key/[provider]/route.ts
await modelRuntime.login(provider, "api_key", {
  notify: () => {},
  prompt: async (prompt) => {
    if (prompt.type === "select") {
      const keyOption = prompt.options.find(o => o.id === "api-key" || o.id === "bearer-token");
      if (keyOption) return keyOption.id;
      throw new Error(`${provider} requires interactive authentication setup`);
    }
    if (!keySubmitted && prompt.type === "secret") {
      keySubmitted = true;
      return apiKey.trim();
    }
    throw new Error(`${provider} requires additional authentication settings`);
  },
});

```

After successful validation, `invalidateModelsCache()` refreshes the available models cache to reflect the newly authenticated provider.

### Revoking Keys and Logout

The `DELETE` method calls `modelRuntime.logout(provider)` to remove stored credentials. For OAuth providers, `POST /api/auth/logout/[provider]` defined in `app/api/auth/logout/[provider]/route.ts` handles logout specifically:

```typescript
// app/api/auth/logout/[provider]/route.ts
if (!modelRuntime.getProvider(provider)?.auth.oauth) {
  return NextResponse.json({ error: "Provider does not support OAuth" }, { status: 400 });
}
await modelRuntime.logout(provider);

```

Both operations trigger cache invalidation to ensure the UI reflects the updated authentication state.

## Session Management and Cache Invalidation

Both authentication flows integrate with [`lib/models-cache.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-cache.ts) to maintain data consistency. The `invalidateModelsCache()` function runs after every successful login, logout, or key deletion to ensure the frontend receives an accurate list of available models for each provider.

The `ModelRuntime` class abstracts credential storage, delegating to the underlying `pi` SDK for secure persistence of OAuth tokens and API keys.

## Frontend Integration Examples

### Initiating OAuth with Server-Sent Events

```typescript
const source = new EventSource(`/api/auth/login/${provider}`);
source.addEventListener('message', async (e) => {
  const data = JSON.parse(e.data);
  if (data.type === 'prompt_request' || data.type === 'select_request') {
    const answer = await showPromptDialog(data.message);
    await fetch(`/api/auth/login/${provider}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: data.token, code: answer })
    });
  }
  if (data.type === 'success') {
    source.close();
  }
});

```

### Submitting an API Key

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

```

### Logging Out

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

```

## Summary

- **Dual authentication support**: `pi-web` implements both OAuth (interactive) and API-Key (direct) authentication through Next.js API routes, delegating credential management to the `ModelRuntime` class.
- **SSE-based OAuth flow**: The OAuth implementation uses Server-Sent Events to push prompts to clients, with token-bound callbacks ensuring secure response routing through the global `__piLoginCallbacks` registry.
- **RESTful API-Key management**: API keys are submitted via POST requests to `app/api/auth/api-key/[provider]/route.ts`, with automatic handling of provider-specific authentication method selection.
- **Cache synchronization**: Both flows invoke `invalidateModelsCache()` from [`lib/models-cache.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-cache.ts) to keep the UI's model listings synchronized with authentication state.
- **Security validation**: All endpoints validate provider support for specific authentication types and verify token ownership before processing credential responses.

## Frequently Asked Questions

### How does pi-web handle OAuth callbacks asynchronously?

The application uses a **token-bound callback pattern** where each OAuth initiation generates a unique token stored in the global `__piLoginCallbacks` registry. When the client responds via POST to `/api/auth/login/:provider`, the server resolves the specific promise waiting in the SSE handler, allowing asynchronous communication between the interactive login process and the HTTP response cycle.

### What happens if a user disconnects during OAuth login?

The SSE handler creates an `AbortController` linked to the incoming request object. If the client closes the connection, the abort signal triggers cleanup of the pending token in the registry and terminates the `ModelRuntime.login` operation, preventing orphaned authentication processes.

### Can API-Key authentication handle different credential types like bearer tokens?

Yes. The API-Key endpoint in `app/api/auth/api-key/[provider]/route.ts` inspects prompt types from `ModelRuntime`. When the runtime presents a `select` prompt with options like `"api-key"` or `"bearer-token"`, the handler automatically selects the appropriate option before injecting the secret value, supporting multiple authentication schemes without manual user intervention.

### Where are authentication credentials stored in pi-web?

The `ModelRuntime` class from `@earendil-works/pi-coding-agent` abstracts credential storage, handling the underlying `pi` SDK operations. The web application itself does not directly access the storage layer; instead, it delegates to `modelRuntime.login()` and `modelRuntime.logout()` methods which manage secure persistence and retrieval of OAuth tokens and API keys.