# Pi Web Authentication Flows: OAuth Device-Code and Manual-Code Implementation

> Explore Pi Web's OAuth authentication flows including device-code and manual-code implementation. Learn how to integrate secure login via Server-Sent Events.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: tutorial
- Published: 2026-07-25

---

**Pi Web supports three OAuth authentication flows—device-code verification, manual-code entry, and interactive select prompts—all orchestrated through Server-Sent Events (SSE) via the Next.js API route `app/api/auth/login/[provider]/route.ts`.**

Pi Web (agegr/pi-web) implements a unified OAuth layer that handles external service authentication without traditional browser redirects. The system streams interactive prompts to the client via SSE, enabling secure device-code verification, manual code entry, and account selection flows within a single endpoint architecture.

## How Pi Web Authentication Flows Work

The authentication system in Pi Web operates on a bidirectional communication pattern using **Server-Sent Events (SSE)** for server-to-client updates and HTTP POST requests for client responses. When a user initiates login, the `ModelRuntime.login()` method from the underlying `@earendil-works/pi-coding-agent` package triggers provider-specific OAuth sequences.

The core coordination happens through an in-memory registry stored at `globalThis.__piLoginCallbacks`. This registry maps temporary tokens to Promise resolvers, allowing the SSE stream to pause while awaiting user input. The main orchestration logic resides in `app/api/auth/login/[provider]/route.ts`, which handles both the initial GET request to open the stream and subsequent POST requests to resolve pending authentication steps.

## Supported Authentication Flows

Pi Web implements three distinct interactive authentication patterns depending on the provider's capabilities and the OAuth requirements.

### Device-Code Flow

The **device-code flow** enables authentication on secondary devices when the client cannot easily input credentials. When the `ModelRuntime` emits an `AuthEvent` of type `device_code`, Pi Web streams a `device_code` SSE event containing the `userCode`, `verificationUri`, and timing hints (lines 51‑58).

```typescript
// Server-side excerpt from route.ts (lines 51-58 concept)
event.type === 'device_code' && 
  write({ 
    type: 'device_code', 
    userCode: event.userCode, 
    verificationUri: event.verificationUri 
  });

```

The client displays the `userCode` and `verificationUri` to the user, who completes authentication on a separate device. Unlike other flows, this requires **no POST request** from the client—the `ModelRuntime` automatically detects completion and resolves the pending promise internally.

### Manual-Code Entry

For providers requiring short verification codes (the "manual code" step of OAuth), Pi Web creates a **one-time token** and streams a `prompt_request` SSE event. This flow uses the `createClientInputRequest` helper function to generate unique tokens and register Promise resolvers in the global registry (lines 22‑25, 40‑41).

```typescript
const createClientInputRequest = () => {
  const token = `${provider}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
  activeTokens.add(token);
  const promise = new Promise<string>((resolve, reject) => {
    registry.set(token, { resolve, reject });
  });
  return { token, promise };
};

```

The frontend collects the code from the user and POSTs it back to `/api/auth/login/[provider]` with the structure `{ token, code }`. The server looks up the token in `globalThis.__piLoginCallbacks`, resolves the Promise with the supplied code, and continues the OAuth handshake.

### Select Prompt

When a provider requires the user to choose from multiple options (such as selecting a specific account or organization), Pi Web sends a `select_request` SSE event containing the available choices (lines 26‑31). The client presents these options in the UI, and the user's selection is returned via POST to the same endpoint using the token-based resolution mechanism.

## Implementation Architecture

### Client-Side Integration

The client establishes an `EventSource` connection to handle the three flow types:

```typescript
const eventSource = new EventSource(`/api/auth/login/${provider}`);

eventSource.onmessage = (e) => {
  const data = JSON.parse(e.data);
  switch (data.type) {
    case 'select_request':
      // Display options UI, then POST user choice
      fetch(`/api/auth/login/${provider}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token: data.token, code: userChoice })
      });
      break;
    case 'prompt_request':
      // Display text input for manual code
      fetch(`/api/auth/login/${provider}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token: data.token, code: inputValue })
      });
      break;
    case 'device_code':
      // Display data.userCode and data.verificationUri
      console.log(`Enter ${data.userCode} at ${data.verificationUri}`);
      break;
    case 'success':
      eventSource.close();
      window.location.reload();
      break;
  }
};

```

### Post-Authentication Cleanup

Upon successful completion of `ModelRuntime.login()`, Pi Web calls `invalidateModelsCache()` from [`lib/models-cache.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-cache.ts) to refresh the available model listings, then streams a final `success` event to close the SSE connection. The temporary tokens are cleaned up from `activeTokens` and the global registry to prevent replay attacks.

## Summary

- **Pi Web authentication flows** support device-code, manual-code, and select-prompt OAuth patterns through a unified SSE endpoint at `app/api/auth/login/[provider]/route.ts`.
- **Device-code flow** streams verification URIs and user codes without requiring client-side polling or POST responses.
- **Manual-code entry** generates one-time tokens via `createClientInputRequest`, resolving pending promises when users submit codes through POST requests.
- **Select prompts** enable multi-option authentication scenarios (like account selection) using the same token-resolution architecture.
- The system relies on `globalThis.__piLoginCallbacks` for state management and invalidates the models cache immediately after successful authentication.

## Frequently Asked Questions

### What authentication protocols does Pi Web support?

Pi Web supports **OAuth 2.0** with extensions for device-code grants (RFC 8628) and manual verification code entry. The implementation handles provider-specific variations—including account selection prompts—through a unified interface in `app/api/auth/login/[provider]/route.ts`.

### How does Pi Web handle device-code authentication without polling?

The device-code flow delegates polling responsibility to the `ModelRuntime` within the `@earendil-works/pi-coding-agent` package. While the client simply displays the `userCode` and `verificationUri`, the server-side runtime continuously checks for authorization completion. Once confirmed, the runtime resolves the internal Promise, allowing Pi Web to stream the `success` event without additional client interaction.

### Where is the authentication state stored during the OAuth handshake?

Pi Web uses an **in-memory registry** at `globalThis.__piLoginCallbacks` to store Promise resolvers and rejection handlers mapped to temporary tokens. This registry coordinates between the SSE stream (which emits prompts) and the POST endpoint (which receives user responses). Active tokens are tracked in a separate `activeTokens` Set for validation during the handshake.

### What happens after successful authentication in Pi Web?

After `ModelRuntime.login()` resolves successfully, Pi Web executes `invalidateModelsCache()` from [`lib/models-cache.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-cache.ts) to refresh the cached model listings. The system then streams a `success` event through the SSE connection, closes the EventSource, and cleans up the temporary token and Promise resolver from the global registry.