Authentication Flows for OAuth, API Key, and Device-Code in Pi-Web
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 codesauth– Provides an OAuth URL for browser-based authorizationdevice_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):
// 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):
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 |
Safe read/write operations for auth.json and credential removal |
lib/models-cache.ts |
Central model cache with invalidateModelsCache() function |
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 viaPOST /api/auth/login/[provider]. - Device-code authentication emits specific SSE events containing
userCodeandverificationUri, 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
ModelRuntimeclass from the@earendil-works/pi-coding-agentSDK 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →