810 lines
21 KiB
Markdown
810 lines
21 KiB
Markdown
# OpenAI OAuth Implementation Plan
|
|
|
|
## Executive Summary
|
|
|
|
Add ChatGPT Plus/Pro OAuth authentication to Flynn, enabling use of OpenAI models (including Codex) via OAuth tokens instead of API keys. This mimics OpenCode's `CodexAuthPlugin` pattern.
|
|
|
|
**Minimal Viable Approach**: Implement OAuth device flow + token refresh + Codex responses endpoint routing.
|
|
|
|
---
|
|
|
|
## Design Overview
|
|
|
|
### Key Components
|
|
|
|
1. **OAuth Module** (`src/auth/openai.ts`) - Device flow + token management
|
|
2. **OpenAI Client Enhancement** (`src/models/openai.ts`) - OAuth token support
|
|
3. **Config Schema Update** (`src/config/schema.ts`) - OAuth config fields
|
|
4. **CLI Command** (`src/cli/commands/login.ts`) - Interactive login
|
|
5. **Testing Strategy** - Unit tests + integration tests
|
|
|
|
### API Surfaces
|
|
|
|
```typescript
|
|
// src/auth/openai.ts
|
|
export interface OpenAIAuthStore {
|
|
id_token: string;
|
|
access_token: string;
|
|
refresh_token: string;
|
|
expires_at: number;
|
|
account_id?: string;
|
|
}
|
|
|
|
export interface DeviceAuthResponse {
|
|
device_auth_id: string;
|
|
user_code: string;
|
|
interval: string;
|
|
}
|
|
|
|
export function requestOpenAIDeviceCode(): Promise<DeviceAuthResponse>
|
|
export function pollForOpenAIToken(deviceAuthId: string, userCode: string, interval: number): Promise<TokenResponse>
|
|
export function refreshOpenAIToken(refreshToken: string): Promise<TokenResponse>
|
|
export function loadOpenAIToken(): OpenAIAuthStore | null
|
|
export function storeOpenAIToken(tokens: TokenResponse): void
|
|
export function getOpenAIToken(): Promise<OpenAIAuthStore | null>
|
|
export function loginOpenAI(onPrompt: (userCode: string, url: string) => void): Promise<OpenAIAuthStore>
|
|
|
|
// src/models/openai.ts - Enhanced config
|
|
export interface OpenAIClientConfig {
|
|
apiKey?: string;
|
|
model: string;
|
|
maxTokens?: number;
|
|
baseURL?: string;
|
|
timeoutMs?: number;
|
|
oauth?: {
|
|
enabled: boolean;
|
|
tokenLoader?: () => Promise<OpenAIAuthStore | null>;
|
|
tokenSaver?: (tokens: OpenAIAuthStore) => Promise<void>;
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Details
|
|
|
|
### 1. OAuth Module (`src/auth/openai.ts`)
|
|
|
|
**Purpose**: Handle OAuth device flow for ChatGPT Plus/Pro authentication.
|
|
|
|
**Key Constants**:
|
|
```typescript
|
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
const ISSUER = 'https://auth.openai.com';
|
|
const CODEX_API_ENDPOINT = 'https://chatgpt.com/backend-api/codex/responses';
|
|
```
|
|
|
|
**Core Functions**:
|
|
|
|
#### a. Device Flow Initiation
|
|
```typescript
|
|
async function requestOpenAIDeviceCode(): Promise<DeviceAuthResponse> {
|
|
// POST to https://auth.openai.com/api/accounts/deviceauth/usercode
|
|
// Body: { client_id: CLIENT_ID }
|
|
// Returns: { device_auth_id, user_code, interval }
|
|
}
|
|
```
|
|
|
|
#### b. Token Polling
|
|
```typescript
|
|
async function pollForOpenAIToken(
|
|
deviceAuthId: string,
|
|
userCode: string,
|
|
interval: number
|
|
): Promise<TokenResponse> {
|
|
// Poll https://auth.openai.com/api/accounts/deviceauth/token
|
|
// Body: { device_auth_id, user_code }
|
|
// On success, exchange authorization_code for tokens via /oauth/token
|
|
}
|
|
```
|
|
|
|
#### c. Token Refresh
|
|
```typescript
|
|
async function refreshOpenAIToken(refreshToken: string): Promise<TokenResponse> {
|
|
// POST https://auth.openai.com/oauth/token
|
|
// Body: {
|
|
// grant_type: 'refresh_token',
|
|
// refresh_token: refreshToken,
|
|
// client_id: CLIENT_ID
|
|
// }
|
|
}
|
|
```
|
|
|
|
#### d. Token Storage
|
|
```typescript
|
|
// Storage path: ~/.config/flynn/auth.json
|
|
interface AuthStore {
|
|
github?: { ... };
|
|
openai?: {
|
|
id_token: string;
|
|
access_token: string;
|
|
refresh_token: string;
|
|
expires_at: number;
|
|
account_id?: string;
|
|
};
|
|
}
|
|
```
|
|
|
|
#### e. Account ID Extraction
|
|
```typescript
|
|
function extractAccountId(tokens: TokenResponse): string | undefined {
|
|
// Parse JWT claims from id_token or access_token
|
|
// Priority:
|
|
// 1. claims.chatgpt_account_id
|
|
// 2. claims['https://api.openai.com/auth'].chatgpt_account_id
|
|
// 3. claims.organizations[0].id
|
|
}
|
|
```
|
|
|
|
**File Structure**:
|
|
```
|
|
src/auth/
|
|
├── github.ts # Existing
|
|
├── openai.ts # NEW - OAuth device flow
|
|
└── index.ts # Export openai.ts exports
|
|
```
|
|
|
|
---
|
|
|
|
### 2. OpenAI Client Enhancement (`src/models/openai.ts`)
|
|
|
|
**Changes**:
|
|
|
|
#### a. Config Extension
|
|
```typescript
|
|
export interface OpenAIClientConfig {
|
|
apiKey?: string; // Now optional when OAuth is used
|
|
model: string;
|
|
maxTokens?: number;
|
|
baseURL?: string;
|
|
timeoutMs?: number;
|
|
oauth?: {
|
|
enabled: boolean;
|
|
tokenLoader?: () => Promise<OpenAIAuthStore | null>;
|
|
tokenSaver?: (tokens: OpenAIAuthStore) => Promise<void>;
|
|
};
|
|
}
|
|
```
|
|
|
|
#### b. Client Constructor Changes
|
|
```typescript
|
|
constructor(config: OpenAIClientConfig) {
|
|
this.oauthConfig = config.oauth;
|
|
this.client = new OpenAI({
|
|
apiKey: config.apiKey ?? 'dummy-key-for-oauth', // OpenAI SDK requires this
|
|
baseURL: config.baseURL,
|
|
timeout: config.timeoutMs ?? 20_000,
|
|
maxRetries: 0,
|
|
fetch: this.oauthConfig?.enabled ? this.createOAuthFetch() : undefined,
|
|
});
|
|
// ...
|
|
}
|
|
```
|
|
|
|
#### c. Custom Fetch for OAuth
|
|
```typescript
|
|
private createOAuthFetch() {
|
|
return async (url: RequestInfo | URL, init?: RequestInit) => {
|
|
if (!this.oauthConfig?.tokenLoader) {
|
|
throw new Error('OAuth enabled but no token loader provided');
|
|
}
|
|
|
|
// Load token
|
|
let auth = await this.oauthConfig.tokenLoader();
|
|
|
|
// Refresh if expired
|
|
if (!auth || auth.expires_at < Date.now()) {
|
|
if (!auth?.refresh_token) {
|
|
throw new Error('OAuth token expired and no refresh token available');
|
|
}
|
|
const tokens = await refreshOpenAIToken(auth.refresh_token);
|
|
auth = {
|
|
id_token: tokens.id_token,
|
|
access_token: tokens.access_token,
|
|
refresh_token: tokens.refresh_token,
|
|
expires_at: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
account_id: extractAccountId(tokens) || auth.account_id,
|
|
};
|
|
if (this.oauthConfig.tokenSaver) {
|
|
await this.oauthConfig.tokenSaver(auth);
|
|
}
|
|
}
|
|
|
|
// Build headers
|
|
const headers = new Headers(init?.headers);
|
|
headers.set('Authorization', `Bearer ${auth.access_token}`);
|
|
if (auth.account_id) {
|
|
headers.set('ChatGPT-Account-Id', auth.account_id);
|
|
}
|
|
headers.set('originator', 'flynn');
|
|
|
|
// Rewrite URL to Codex endpoint for supported models
|
|
const isCodexModel = this.model.includes('codex') ||
|
|
['gpt-5.1', 'gpt-5.2', 'gpt-5.3'].some(v => this.model.includes(v));
|
|
const targetUrl = isCodexModel ?
|
|
new URL(CODEX_API_ENDPOINT) :
|
|
(typeof url === 'string' ? new URL(url) : url);
|
|
|
|
return fetch(targetUrl, { ...init, headers });
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Config Schema Update (`src/config/schema.ts`)
|
|
|
|
**Changes**:
|
|
|
|
```typescript
|
|
// Line ~46-56: Enhance modelConfigBaseSchema
|
|
const modelConfigBaseSchema = z.object({
|
|
provider: z.enum(MODEL_PROVIDERS),
|
|
model: z.string(),
|
|
endpoint: z.string().optional(),
|
|
api_key: z.string().optional(),
|
|
auth_token: z.string().optional(),
|
|
oauth_enabled: z.boolean().optional(), // NEW
|
|
for: z.array(z.string()).optional(),
|
|
num_gpu: z.number().optional(),
|
|
context_window: z.number().optional(),
|
|
supports_audio: z.boolean().optional(),
|
|
});
|
|
```
|
|
|
|
**Example Config**:
|
|
```yaml
|
|
models:
|
|
default:
|
|
provider: openai
|
|
model: gpt-5.2-codex
|
|
oauth_enabled: true # Use OAuth instead of API key
|
|
```
|
|
|
|
---
|
|
|
|
### 4. Model Factory Update (`src/daemon/models.ts`)
|
|
|
|
**Changes**:
|
|
|
|
```typescript
|
|
// Line ~51-55: createClientFromConfig() openai case
|
|
case 'openai':
|
|
return new OpenAIClient({
|
|
model: cfg.model,
|
|
apiKey: cfg.api_key,
|
|
oauth: cfg.oauth_enabled ? {
|
|
enabled: true,
|
|
tokenLoader: async () => {
|
|
const { getOpenAIToken } = await import('../auth/openai.js');
|
|
return getOpenAIToken();
|
|
},
|
|
tokenSaver: async (tokens) => {
|
|
const { storeOpenAIToken } = await import('../auth/openai.js');
|
|
storeOpenAIToken(tokens);
|
|
},
|
|
} : undefined,
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### 5. CLI Command (`src/cli/commands/login.ts`)
|
|
|
|
**New File**:
|
|
|
|
```typescript
|
|
import { Command } from 'commander';
|
|
import { loginOpenAI } from '../../auth/openai.js';
|
|
|
|
export function createLoginCommand(): Command {
|
|
const cmd = new Command('login');
|
|
|
|
cmd
|
|
.command('openai')
|
|
.description('Authenticate with OpenAI using ChatGPT Plus/Pro account')
|
|
.action(async () => {
|
|
console.log('Starting OpenAI OAuth login...\n');
|
|
|
|
try {
|
|
const auth = await loginOpenAI((userCode, url) => {
|
|
console.log(`\nPlease visit: ${url}`);
|
|
console.log(`Enter code: ${userCode}\n`);
|
|
console.log('Waiting for authorization...');
|
|
});
|
|
|
|
console.log('\n✓ Successfully authenticated with OpenAI!');
|
|
if (auth.account_id) {
|
|
console.log(` Account ID: ${auth.account_id}`);
|
|
}
|
|
console.log('\nYou can now use OpenAI models with oauth_enabled: true in your config.');
|
|
} catch (error) {
|
|
console.error('Login failed:', error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
cmd
|
|
.command('github')
|
|
.description('Authenticate with GitHub Copilot')
|
|
.action(async () => {
|
|
const { loginGitHub } = await import('../../auth/github.js');
|
|
// ... existing GitHub login logic
|
|
});
|
|
|
|
return cmd;
|
|
}
|
|
```
|
|
|
|
**Register in** `src/cli/index.ts`:
|
|
```typescript
|
|
import { createLoginCommand } from './commands/login.js';
|
|
// ...
|
|
program.addCommand(createLoginCommand());
|
|
```
|
|
|
|
---
|
|
|
|
## Exact Flow Steps
|
|
|
|
### User Authentication Flow
|
|
|
|
```
|
|
1. User runs: flynn login openai
|
|
|
|
2. CLI requests device code from OpenAI:
|
|
POST https://auth.openai.com/api/accounts/deviceauth/usercode
|
|
Body: { client_id: 'app_EMoamEEZ73f0CkXaXp7hrann' }
|
|
|
|
3. OpenAI returns:
|
|
{
|
|
device_auth_id: "uuid-v4",
|
|
user_code: "ABCD-1234",
|
|
interval: "5"
|
|
}
|
|
|
|
4. CLI displays:
|
|
"Visit: https://auth.openai.com/codex/device"
|
|
"Enter code: ABCD-1234"
|
|
|
|
5. CLI polls for token:
|
|
POST https://auth.openai.com/api/accounts/deviceauth/token
|
|
Body: { device_auth_id, user_code }
|
|
Every 5 seconds (+ 3s safety margin)
|
|
|
|
6. When user authorizes, OpenAI returns:
|
|
{
|
|
authorization_code: "auth-code-xyz",
|
|
code_verifier: "pkce-verifier"
|
|
}
|
|
|
|
7. CLI exchanges code for tokens:
|
|
POST https://auth.openai.com/oauth/token
|
|
Body: {
|
|
grant_type: 'authorization_code',
|
|
code: authorization_code,
|
|
redirect_uri: 'https://auth.openai.com/deviceauth/callback',
|
|
client_id: CLIENT_ID,
|
|
code_verifier: code_verifier
|
|
}
|
|
|
|
8. OpenAI returns tokens:
|
|
{
|
|
id_token: "jwt-id-token",
|
|
access_token: "jwt-access-token",
|
|
refresh_token: "refresh-token",
|
|
expires_in: 3600
|
|
}
|
|
|
|
9. CLI extracts account_id from JWT claims
|
|
|
|
10. CLI stores to ~/.config/flynn/auth.json:
|
|
{
|
|
openai: {
|
|
id_token: "...",
|
|
access_token: "...",
|
|
refresh_token: "...",
|
|
expires_at: 1739123456789,
|
|
account_id: "org-xxx"
|
|
}
|
|
}
|
|
|
|
11. User updates config.yaml:
|
|
models:
|
|
default:
|
|
provider: openai
|
|
model: gpt-5.2-codex
|
|
oauth_enabled: true
|
|
|
|
12. Flynn daemon starts, creates OpenAI client with OAuth enabled
|
|
```
|
|
|
|
### Request Flow (OAuth Mode)
|
|
|
|
```
|
|
1. User sends message via Telegram/TUI
|
|
|
|
2. Agent calls modelRouter.chat(request)
|
|
|
|
3. OpenAIClient.chat() invoked
|
|
|
|
4. Custom fetch interceptor:
|
|
a. Load token from ~/.config/flynn/auth.json
|
|
b. Check if expires_at < Date.now()
|
|
c. If expired:
|
|
- POST refresh token to /oauth/token
|
|
- Update stored token
|
|
d. Set headers:
|
|
- Authorization: Bearer {access_token}
|
|
- ChatGPT-Account-Id: {account_id}
|
|
- originator: flynn
|
|
e. Rewrite URL to Codex endpoint if model includes 'codex'
|
|
f. Execute fetch with modified request
|
|
|
|
5. OpenAI returns response (free for ChatGPT Plus/Pro subscribers)
|
|
|
|
6. Response parsed and returned to agent
|
|
```
|
|
|
|
---
|
|
|
|
## File Changes Summary
|
|
|
|
### New Files
|
|
```
|
|
src/auth/openai.ts # OAuth device flow (300 lines)
|
|
src/cli/commands/login.ts # Login command (80 lines)
|
|
docs/plans/openai-oauth-implementation.md # This document
|
|
```
|
|
|
|
### Modified Files
|
|
```
|
|
src/auth/index.ts # Export openai.ts functions (10 lines changed)
|
|
src/models/openai.ts # Add OAuth support (150 lines changed)
|
|
src/config/schema.ts # Add oauth_enabled field (5 lines changed)
|
|
src/daemon/models.ts # Add OAuth tokenLoader/Saver (15 lines changed)
|
|
src/cli/index.ts # Register login command (3 lines changed)
|
|
```
|
|
|
|
### Test Files (New)
|
|
```
|
|
src/auth/openai.test.ts # Unit tests for OAuth flow
|
|
src/models/openai.oauth.test.ts # Integration tests for OAuth client
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
#### 1. JWT Parsing (`src/auth/openai.test.ts`)
|
|
```typescript
|
|
describe('parseJwtClaims', () => {
|
|
test('parses valid JWT', () => {
|
|
const token = createTestJwt({ email: 'test@example.com' });
|
|
expect(parseJwtClaims(token)).toMatchObject({ email: 'test@example.com' });
|
|
});
|
|
|
|
test('returns undefined for invalid JWT', () => {
|
|
expect(parseJwtClaims('invalid')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('extractAccountId', () => {
|
|
test('extracts from chatgpt_account_id', () => {
|
|
const tokens = {
|
|
id_token: createTestJwt({ chatgpt_account_id: 'acc-123' }),
|
|
access_token: '',
|
|
refresh_token: '',
|
|
};
|
|
expect(extractAccountId(tokens)).toBe('acc-123');
|
|
});
|
|
|
|
test('extracts from organizations array', () => {
|
|
const tokens = {
|
|
id_token: createTestJwt({ organizations: [{ id: 'org-123' }] }),
|
|
access_token: '',
|
|
refresh_token: '',
|
|
};
|
|
expect(extractAccountId(tokens)).toBe('org-123');
|
|
});
|
|
});
|
|
```
|
|
|
|
#### 2. Token Refresh Logic (`src/models/openai.oauth.test.ts`)
|
|
```typescript
|
|
describe('OpenAIClient OAuth', () => {
|
|
test('refreshes expired token before request', async () => {
|
|
const expiredAuth = {
|
|
access_token: 'expired',
|
|
refresh_token: 'valid-refresh',
|
|
expires_at: Date.now() - 1000, // Expired
|
|
};
|
|
|
|
const client = new OpenAIClient({
|
|
model: 'gpt-5.2-codex',
|
|
oauth: {
|
|
enabled: true,
|
|
tokenLoader: async () => expiredAuth,
|
|
tokenSaver: vi.fn(),
|
|
},
|
|
});
|
|
|
|
// Mock refreshOpenAIToken
|
|
vi.mock('../auth/openai.js', () => ({
|
|
refreshOpenAIToken: vi.fn().mockResolvedValue({
|
|
access_token: 'new-access',
|
|
refresh_token: 'valid-refresh',
|
|
expires_in: 3600,
|
|
}),
|
|
}));
|
|
|
|
await client.chat({
|
|
messages: [{ role: 'user', content: 'test' }],
|
|
});
|
|
|
|
expect(refreshOpenAIToken).toHaveBeenCalledWith('valid-refresh');
|
|
});
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
#### 3. End-to-End OAuth Flow (Manual)
|
|
```bash
|
|
# 1. Run login command
|
|
flynn login openai
|
|
|
|
# 2. Verify token stored
|
|
cat ~/.config/flynn/auth.json | jq '.openai'
|
|
|
|
# 3. Start daemon with OAuth config
|
|
cat > /tmp/flynn-oauth-test.yaml <<EOF
|
|
models:
|
|
default:
|
|
provider: openai
|
|
model: gpt-5.2-codex
|
|
oauth_enabled: true
|
|
telegram:
|
|
bot_token: \${TELEGRAM_BOT_TOKEN}
|
|
allowed_chat_ids: [123456789]
|
|
EOF
|
|
|
|
flynn start --config /tmp/flynn-oauth-test.yaml
|
|
|
|
# 4. Send test message and verify response
|
|
```
|
|
|
|
### Mock Testing Setup
|
|
|
|
```typescript
|
|
// test/mocks/openai-oauth.ts
|
|
export function mockOpenAIAuthEndpoints() {
|
|
return {
|
|
deviceCode: vi.fn().mockResolvedValue({
|
|
device_auth_id: 'test-device-id',
|
|
user_code: 'ABCD-1234',
|
|
interval: '1',
|
|
}),
|
|
deviceToken: vi.fn().mockResolvedValue({
|
|
authorization_code: 'test-auth-code',
|
|
code_verifier: 'test-verifier',
|
|
}),
|
|
exchangeToken: vi.fn().mockResolvedValue({
|
|
id_token: createTestJwt({ chatgpt_account_id: 'acc-test' }),
|
|
access_token: createTestJwt({ sub: 'user-123' }),
|
|
refresh_token: 'test-refresh',
|
|
expires_in: 3600,
|
|
}),
|
|
refreshToken: vi.fn().mockResolvedValue({
|
|
access_token: 'new-access-token',
|
|
refresh_token: 'test-refresh',
|
|
expires_in: 3600,
|
|
}),
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Migration & Rollout
|
|
|
|
### Phase 1: Core Implementation (Week 1)
|
|
- [ ] Implement `src/auth/openai.ts` with device flow
|
|
- [ ] Add unit tests for JWT parsing and account ID extraction
|
|
- [ ] Update `src/models/openai.ts` with OAuth support
|
|
- [ ] Add mock-based tests for OAuth client
|
|
|
|
### Phase 2: CLI & Config (Week 2)
|
|
- [ ] Implement `flynn login openai` command
|
|
- [ ] Update config schema for `oauth_enabled`
|
|
- [ ] Update daemon model factory
|
|
- [ ] Manual end-to-end testing
|
|
|
|
### Phase 3: Documentation & Polish (Week 3)
|
|
- [ ] Add user documentation (USAGE.md)
|
|
- [ ] Add examples to README.md
|
|
- [ ] Error handling improvements (expired subscriptions, network failures)
|
|
- [ ] Add token status command: `flynn auth status`
|
|
|
|
---
|
|
|
|
## Known Limitations & Future Enhancements
|
|
|
|
### Current Limitations
|
|
1. **No browser-based OAuth flow** - Only device flow (headless-friendly)
|
|
2. **No automatic subscription verification** - Assumes user has ChatGPT Plus/Pro
|
|
3. **Single account support** - No multi-account switching
|
|
|
|
### Future Enhancements
|
|
1. **Browser OAuth flow** - Add local HTTP server for callback (like OpenCode)
|
|
2. **Subscription checker** - Verify user has Plus/Pro before allowing config
|
|
3. **Token rotation** - Automatic background refresh before expiry
|
|
4. **Multi-account** - Support multiple OpenAI accounts with profiles
|
|
5. **TUI integration** - In-app login flow instead of separate CLI command
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
1. **Token Storage**:
|
|
- Store in `~/.config/flynn/auth.json` with `chmod 600`
|
|
- Never log access tokens in debug output
|
|
- Clear tokens on logout: `flynn logout openai`
|
|
|
|
2. **Token Refresh**:
|
|
- Refresh tokens valid for ~6 months (OpenAI policy)
|
|
- Handle refresh failures gracefully (prompt re-login)
|
|
|
|
3. **Rate Limiting**:
|
|
- ChatGPT Plus/Pro has usage limits (not documented publicly)
|
|
- Implement exponential backoff on 429 responses
|
|
|
|
4. **PKCE Verification**:
|
|
- Device flow includes code_verifier for PKCE
|
|
- Prevent CSRF by validating state (future browser flow)
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
**No new dependencies required** - Uses existing:
|
|
- `openai` (already in package.json)
|
|
- Native `fetch` API (Node.js >=18)
|
|
- Native `crypto` API for JWT parsing
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
1. ✅ User can run `flynn login openai` and authenticate
|
|
2. ✅ Token persists across daemon restarts
|
|
3. ✅ Expired tokens refresh automatically
|
|
4. ✅ OpenAI Codex models work via OAuth (free for Plus/Pro users)
|
|
5. ✅ Fallback to API key mode if `oauth_enabled: false`
|
|
6. ✅ Error messages guide users to fix auth issues
|
|
|
|
---
|
|
|
|
## API Endpoint Reference
|
|
|
|
### OpenAI OAuth Endpoints
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `https://auth.openai.com/api/accounts/deviceauth/usercode` | POST | Initiate device flow |
|
|
| `https://auth.openai.com/api/accounts/deviceauth/token` | POST | Poll for authorization |
|
|
| `https://auth.openai.com/oauth/token` | POST | Exchange code / refresh token |
|
|
| `https://chatgpt.com/backend-api/codex/responses` | POST | Codex completions endpoint |
|
|
|
|
### Request/Response Formats
|
|
|
|
#### Device Code Request
|
|
```json
|
|
POST /api/accounts/deviceauth/usercode
|
|
{
|
|
"client_id": "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"device_auth_id": "uuid-v4",
|
|
"user_code": "ABCD-1234",
|
|
"interval": "5"
|
|
}
|
|
```
|
|
|
|
#### Token Polling Request
|
|
```json
|
|
POST /api/accounts/deviceauth/token
|
|
{
|
|
"device_auth_id": "uuid-v4",
|
|
"user_code": "ABCD-1234"
|
|
}
|
|
```
|
|
|
|
**Response (pending)**:
|
|
```json
|
|
{ "status": 403 } // Keep polling
|
|
```
|
|
|
|
**Response (authorized)**:
|
|
```json
|
|
{
|
|
"authorization_code": "auth-code-xyz",
|
|
"code_verifier": "pkce-verifier"
|
|
}
|
|
```
|
|
|
|
#### Token Exchange Request
|
|
```json
|
|
POST /oauth/token
|
|
Content-Type: application/x-www-form-urlencoded
|
|
|
|
grant_type=authorization_code
|
|
&code=auth-code-xyz
|
|
&redirect_uri=https://auth.openai.com/deviceauth/callback
|
|
&client_id=app_EMoamEEZ73f0CkXaXp7hrann
|
|
&code_verifier=pkce-verifier
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"id_token": "eyJhbGc...",
|
|
"access_token": "eyJhbGc...",
|
|
"refresh_token": "refresh-token-xyz",
|
|
"expires_in": 3600,
|
|
"token_type": "Bearer"
|
|
}
|
|
```
|
|
|
|
#### Refresh Token Request
|
|
```json
|
|
POST /oauth/token
|
|
Content-Type: application/x-www-form-urlencoded
|
|
|
|
grant_type=refresh_token
|
|
&refresh_token=refresh-token-xyz
|
|
&client_id=app_EMoamEEZ73f0CkXaXp7hrann
|
|
```
|
|
|
|
**Response**: Same as token exchange
|
|
|
|
#### Codex Request
|
|
```json
|
|
POST /backend-api/codex/responses
|
|
Authorization: Bearer {access_token}
|
|
ChatGPT-Account-Id: {account_id}
|
|
originator: flynn
|
|
|
|
{
|
|
"model": "gpt-5.2-codex",
|
|
"messages": [...],
|
|
"max_tokens": 4096
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Conclusion
|
|
|
|
This plan provides a **minimal, production-ready** OAuth implementation for Flynn, closely modeled after OpenCode's proven `CodexAuthPlugin`. The device flow approach is headless-friendly and aligns with Flynn's daemon architecture.
|
|
|
|
**Total implementation effort**: ~600 lines of new code + ~200 lines of modifications + comprehensive tests.
|
|
|
|
**Key advantages**:
|
|
- Free ChatGPT Plus/Pro model access (no API costs)
|
|
- Proven OAuth flow (matches OpenCode)
|
|
- Clean separation of concerns (auth module + client enhancement)
|
|
- Backward compatible (API key mode still works)
|
|
|
|
**Next steps**:
|
|
1. Review this plan
|
|
2. Implement Phase 1 (core OAuth + tests)
|
|
3. Test with real ChatGPT Plus account
|
|
4. Iterate on UX and error handling
|