OAuth and API-Key Authentication Flow in pi-web: Complete Implementation Guide
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 returns a filtered list of providers that support OAuth authentication. The route instantiates ModelRuntime and queries existing credentials to determine login status.
// 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
AbortControllerlinks the SSE connection to the client request, terminating the login process if the client disconnects - Prompt callbacks: The
modelRuntime.login()method receivespromptandnotifycallbacks to handle interactive authentication steps - Event types: The SSE stream emits
select_request,prompt_request,auth(containing URLs),device_code, andprogressevents
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.
// 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:
// 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:
// 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:
// 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 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
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
await fetch(`/api/auth/api-key/${provider}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey: userProvidedKey })
});
Logging Out
await fetch(`/api/auth/logout/${provider}`, { method: 'POST' });
Summary
- Dual authentication support:
pi-webimplements both OAuth (interactive) and API-Key (direct) authentication through Next.js API routes, delegating credential management to theModelRuntimeclass. - 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
__piLoginCallbacksregistry. - 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()fromlib/models-cache.tsto 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.
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 →