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

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).

// 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).

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:

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 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 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.

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 →