Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
8e1c2c9
fix: preserve Anthropic stream input usage
lidge-jun Jun 23, 2026
5ae187e
fix: use additive merge for Anthropic stream usage
lidge-jun Jun 23, 2026
ccbcd6c
feat: add CodexAccount types and account credential store
lidge-jun Jun 23, 2026
f692f59
feat: inject codex account override in passthrough + session affinity
lidge-jun Jun 23, 2026
19bd067
feat: add codex-auth management API endpoints
lidge-jun Jun 23, 2026
c2ea897
feat: add Codex Auth dashboard tab with account pool management
lidge-jun Jun 23, 2026
ce868ad
test: add multi-account auth tests for store CRUD and passthrough ove…
lidge-jun Jun 23, 2026
e379e58
fix: CodexAuth UI — always show main account, add missing CSS
lidge-jun Jun 23, 2026
0f49e43
feat: show real Codex account info in Codex Auth tab
lidge-jun Jun 23, 2026
2efa629
feat: add account import modal + extend cache TTL to 5 minutes
lidge-jun Jun 23, 2026
4e5166a
fix: two-step Add Account modal — pick method then import
lidge-jun Jun 24, 2026
b406580
feat: add ChatGPT OAuth login flow for multi-account
lidge-jun Jun 24, 2026
8398aac
feat: capture quota from passthrough headers + auto-switch at threshold
lidge-jun Jun 24, 2026
8ade42d
fix: add refresh lock, input validation, graceful token fallback
lidge-jun Jun 24, 2026
d0b9ed0
test: add codex-auth API endpoint tests
lidge-jun Jun 24, 2026
63299b8
feat: production OAuth flow — auto-register pool account + GUI polling
lidge-jun Jun 24, 2026
c45db2a
fix: correct OAuth to auth.openai.com with proper client_id and form-…
lidge-jun Jun 24, 2026
0f846d9
test: add OAuth JWT extraction and session affinity tests (27 new)
lidge-jun Jun 24, 2026
b3f4c9f
fix: use official Codex redirect URI (localhost:1455/auth/callback)
lidge-jun Jun 24, 2026
3e58746
fix: actively fetch quota for pool accounts via wham/usage API
lidge-jun Jun 24, 2026
cca29ac
fix: make btn-icon visible in dark mode
lidge-jun Jun 24, 2026
a0b889d
fix: prevent OAuth token collision between pool accounts and Codex CLI
lidge-jun Jun 24, 2026
43fdfb9
fix: show OAuth error on pick step + clean unused constructor param
lidge-jun Jun 24, 2026
a12e21a
fix: address all critical/high issues from ChatGPT code review
lidge-jun Jun 24, 2026
62fd2d0
fix: auto-register chatgpt passthrough provider on startup
lidge-jun Jun 24, 2026
8451d62
fix: sync models_cache.json on server startup for CLI model picker
lidge-jun Jun 24, 2026
dc87e00
fix: clear model cache before catalog refresh on provider add/delete
lidge-jun Jun 24, 2026
0721123
fix: write expired cache wrapper to force CLI model refresh
lidge-jun Jun 24, 2026
c2e3cbb
fix: isolate opencodex auth state in tests
lidge-jun Jun 24, 2026
8da417c
fix: recover codex auth login status
lidge-jun Jun 24, 2026
35b3f0e
fix: refresh codex pool quota on account load
lidge-jun Jun 24, 2026
ce1c1d4
fix: remove personal codex account examples
lidge-jun Jun 24, 2026
955b614
fix: make codex account remove icon red
lidge-jun Jun 24, 2026
cbd9c28
feat: add codex auth quota refresh
lidge-jun Jun 24, 2026
56938f3
fix: label codex pool accounts in request logs
lidge-jun Jun 24, 2026
3342b20
fix: sync codex auth runtime config
lidge-jun Jun 24, 2026
aa4e7b8
fix: cancel codex oauth login flow
lidge-jun Jun 24, 2026
0683378
fix: copy codex oauth login link
lidge-jun Jun 24, 2026
de2f31e
fix: scale codex account pool refresh
lidge-jun Jun 24, 2026
6c2d7df
fix: sync codex oauth login runtime config
lidge-jun Jun 24, 2026
f891486
fix: align codex quota reset rows
lidge-jun Jun 24, 2026
5f95635
fix: allow team codex account members
lidge-jun Jun 24, 2026
7edba42
refactor: split codex auth collision checks
lidge-jun Jun 24, 2026
5a62f47
docs: record codex auth verification inventory
lidge-jun Jun 24, 2026
0572456
docs: plan codex auth verification goal
lidge-jun Jun 24, 2026
cd0b004
docs: refine codex auth verification plan
lidge-jun Jun 24, 2026
dca0823
feat: fail over unhealthy codex pool accounts
lidge-jun Jun 24, 2026
15c2050
docs: record codex auth build verification
lidge-jun Jun 24, 2026
fbe039f
docs: record codex auth runtime verification
lidge-jun Jun 24, 2026
7f7abdb
docs: summarize codex auth verification results
lidge-jun Jun 24, 2026
09aca68
docs: mark codex auth e2e verification complete
lidge-jun Jun 24, 2026
35e4ff6
fix: contain codex auth quota rows
lidge-jun Jun 24, 2026
8b97812
docs: plan codex multi-auth security fixes
lidge-jun Jun 24, 2026
f30fa2d
docs: refine codex multi-auth security plan
lidge-jun Jun 24, 2026
1f0813c
fix: fail closed codex pool auth context
lidge-jun Jun 24, 2026
0efb547
fix: purge codex account lifecycle state
lidge-jun Jun 24, 2026
278873a
fix: guard codex credential refresh generation
lidge-jun Jun 24, 2026
2204da8
fix: bind codex account lifecycle to generations
lidge-jun Jun 24, 2026
9490cfc
fix: guard non-loopback opencodex APIs
lidge-jun Jun 24, 2026
b9903be
fix: disable unverified codex manual import
lidge-jun Jun 24, 2026
820fd8c
fix: treat unknown codex quota conservatively
lidge-jun Jun 24, 2026
35d28a0
fix: classify codex upstream outcomes
lidge-jun Jun 24, 2026
cfc6e47
fix: record codex sidecar auth outcomes
lidge-jun Jun 24, 2026
56c9369
fix: cooldown rate-limited codex accounts
lidge-jun Jun 24, 2026
e123339
fix: record codex terminal stream outcomes
lidge-jun Jun 24, 2026
e752fab
fix: redact codex auth privacy surfaces
lidge-jun Jun 24, 2026
10354c4
docs: record codex privacy verification evidence
lidge-jun Jun 24, 2026
d65877b
fix: bound codex affinity and refresh grants
lidge-jun Jun 24, 2026
72d83d1
fix: avoid privacy scan self-match
lidge-jun Jun 24, 2026
d0cf0aa
docs: record codex phase 70 evidence
lidge-jun Jun 24, 2026
4d9d9f9
fix: preserve invalid config before fallback
lidge-jun Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ jobs:
- name: Test
run: bun test tests

- name: Privacy scan
run: bun run privacy:scan

- name: Check release helper syntax
run: bun build scripts/release.ts --target=bun --outdir=.tmp/ci-release-script-check

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Phase 12: Production-Level Verification (120-129)

> Superseded security note (2026-06-25): This document predates the 280 security patch plan and Phase 10-60 hardening. Treat release-readiness, full-email UI, ordinal request-log labels, unauthenticated management API, fail-open fallback, and earlier account-boundary claims here as historical only. Current merge/deploy evidence is tracked under `devlog/280_codex-multi-auth-security-patch-plan/` and `devlog/_plan/260624_codex-multi-auth-security-implementation/`.

PABCD 12턴: Phase 11 수정 후 프로덕션 수준 검증

## Objective

Phase 11에서 수정된 OAuth flow가 **실제 환경에서 E2E로 작동**하는지 검증한다.
참고 레포들의 테스트/검증 패턴을 적용한다.

## Reference Verification Patterns

| Repo | 검증 방식 |
|------|----------|
| codex-multi-auth | Unit + integration + E2E, health check, rotation test |
| codex-lb | Dashboard health, model catalog, usage tracking |
| term-llm | Manual OAuth flow + security review (sam.saffron 방식) |
| codex-oauth | AuthManager auto-refresh cycle |

## Verification Stages

### Stage 1: OAuth Flow E2E (수동)

**1.1 Login Flow**
```
1. 서버 시작: bun run dev
2. 브라우저: http://localhost:10100 → Codex Auth 탭
3. "+ Add" → "Log in with ChatGPT" 클릭
4. 브라우저 새 탭: auth.openai.com 로그인 페이지 표시 확인
5. ChatGPT 로그인 완료
6. 콜백: http://127.0.0.1:19191/callback?code=...&state=... 수신
7. "Login complete" 페이지 표시
8. GUI: 새 계정이 pool에 표시됨 (email, plan, quota bar)
```

**1.2 Token Exchange 검증**
```bash
# 서버 로그에서 확인:
# - POST auth.openai.com/oauth/token 200 OK
# - access_token, refresh_token, id_token 수신
# - JWT에서 account_id 추출 성공
```

**1.3 실패 시나리오 (redirect URI 문제)**

만약 `:19191`이 거부되면:
```
에러: "redirect_uri_mismatch" 또는 "invalid_redirect_uri"
해결: CALLBACK_PORT를 1455로 변경, CALLBACK_PATH를 /auth/callback으로 변경
```

### Stage 2: Token Refresh Cycle

**2.1 자동 갱신 테스트**
```
1. OAuth로 계정 추가 (Stage 1 완료 후)
2. codex-accounts.json에서 expiresAt를 현재 시간으로 수정 (만료 강제)
3. 해당 계정을 active로 설정
4. Codex App에서 메시지 전송
5. server.ts가 getValidCodexToken → expired → refresh → 성공
6. codex-accounts.json의 expiresAt가 갱신됨
```

**2.2 Refresh 실패 복구**
```
1. codex-accounts.json의 refreshToken을 "invalid" 로 수정
2. 메시지 전송
3. getValidCodexToken → refresh 실패 → throw
4. server.ts catch → fallback to main passthrough
5. 메시지 정상 전달 (main account으로)
6. GUI: 해당 계정에 경고 표시 (실패했지만 서비스는 유지)
```

**2.3 Concurrent Refresh Lock**
```
1. 동시에 2개 요청 전송 (같은 expired 계정)
2. 첫 번째 요청이 refresh lock 획득
3. 두 번째 요청이 같은 Promise 대기
4. 하나의 refresh만 실행됨 (refresh_token 무효화 방지)
```

### Stage 3: Multi-Account Rotation

**3.1 Session Affinity**
```
1. Pool 계정 2개 추가 (A, B)
2. A를 active로 설정
3. thread-1에서 메시지 전송 → A 사용
4. B를 active로 변경
5. thread-1에서 메시지 전송 → 여전히 A (affinity)
6. thread-2에서 메시지 전송 → B 사용
```

**3.2 Auto-Switch**
```
1. A를 active, threshold 80%
2. A의 weeklyPercent를 85%로 설정 (updateAccountQuota)
3. 새 thread에서 메시지 전송
4. resolveCodexAccountForThread → B로 자동 전환
5. config.json의 activeCodexAccountId가 B로 변경됨
```

**3.3 All at Threshold**
```
1. A, B 모두 weeklyPercent 90%
2. 새 thread → 현재 active 유지 (무한 순환 방지)
3. 서비스 중단 없음
```

### Stage 4: Passthrough Injection

**4.1 Header Rewrite 확인**
```bash
# 서버에서 upstream 요청 가로채기 (debug 로그):
# Before: Authorization: Bearer <main-token>
# ChatGPT-Account-Id: <main-id>
# After: Authorization: Bearer <pool-token>
# ChatGPT-Account-Id: <pool-id>
```

**4.2 Quota Header Capture**
```bash
# upstream 응답 헤더에서 확인:
# x-codex-primary-used-percent: 12.5
# x-codex-secondary-used-percent: 45.0
# → updateAccountQuota 호출 → GUI quota bar 갱신
```

### Stage 5: Security Audit

**5.1 Token Exposure**
```bash
# API 응답에서 토큰 미노출 확인:
curl -s localhost:10100/api/codex-auth/accounts | jq '.accounts[].hasCredential'
# true/false만, accessToken/refreshToken 없음
```

**5.2 File Permissions**
```bash
stat -f "%Lp" ~/.opencodex/codex-accounts.json
# 600 (owner read/write only)
```

**5.3 CSRF Protection**
```
1. OAuth state 파라미터가 callback에서 검증됨
2. state mismatch → "State mismatch - possible CSRF attack" 에러
```

**5.4 Input Validation**
```bash
# Oversized input:
curl -X POST localhost:10100/api/codex-auth/accounts \
-H "Content-Type: application/json" \
-d '{"id":"a]...65chars...","email":"x","accessToken":"t","refreshToken":"r","chatgptAccountId":"c"}'
# → 400 "Input too large"
```

### Stage 6: Test Suite

**6.1 새 테스트 추가 목록**

```ts
// tests/chatgpt-oauth.test.ts
describe("ChatGPT OAuth", () => {
test("extractAccountId from id_token (level 1: chatgpt_account_id)");
test("extractAccountId from namespaced claim (level 2)");
test("extractAccountId from organizations (level 3)");
test("extractAccountId returns undefined on invalid JWT");
test("extractEmail from JWT payload");
test("credsFromToken extracts all fields including accountId");
test("refreshChatGPTToken uses form-urlencoded");
test("exchangeToken uses form-urlencoded");
test("constants match auth.openai.com endpoints");
});

// tests/session-affinity.test.ts
describe("session affinity", () => {
test("same thread-id returns same account");
test("new thread uses activeCodexAccountId");
test("deleted account falls back to null");
test("auto-switch triggers at threshold");
test("auto-switch skips when all at threshold");
});

// tests/codex-account-store.test.ts (확장)
describe("token refresh", () => {
test("uses form-urlencoded content-type");
test("concurrent refresh returns same promise (lock)");
test("updates codex-accounts.json on successful refresh");
});
```

**6.2 테스트 목표**
- OAuth: 9 tests
- Session affinity: 5 tests
- Account store 확장: 3 tests
- 기존: 12 tests
- **Total: 29+ tests**

### Stage 7: GUI Verification

```
1. Main Account 표시: email, plan, quota bars
2. Pool accounts 표시: email, quota, active badge
3. Add modal: Import (JSON paste) + OAuth (browser login)
4. OAuth 진행 중: spinner + "Waiting for browser login..." 표시
5. OAuth 완료: 자동 닫힘 + 계정 목록 새로고침
6. OAuth 에러: 에러 메시지 + 재시도 버튼
7. Delete: 확인 dialog → 삭제 → active 리셋
8. Switch: 클릭 → toast "Switched to ..."
9. Auto-switch threshold: 슬라이더 → 즉시 저장
```

## Pass Criteria

| 항목 | 기준 |
|------|------|
| OAuth Login | auth.openai.com 로그인 → token 수신 → pool 등록 |
| Token Refresh | expired → auto refresh → 서비스 유지 |
| Multi-Account | affinity + auto-switch + fallback 정상 |
| Security | 토큰 미노출, 파일 0600, CSRF 검증 |
| Tests | 29+ tests 전체 통과 |
| GUI | 전체 flow 브라우저 검증 |
| Typecheck | `bun run typecheck` clean |
| Build | `gui build` success |

## Known Risks

1. **redirect_uri 거부**: `app_EMoamEEZ73f0CkXaXp7hrann`가 `:19191`을 허용하지 않을 수 있음.
Fallback: `:1455/auth/callback`으로 변경.

2. **country/region 제한**: [GitHub issue #14215](https://github.com/openai/codex/issues/14215)에서
`auth.openai.com/oauth/token`이 특정 지역에서 403 반환 보고됨.
한국에서 테스트해야 함.

3. **Rate limit**: 짧은 시간에 여러 refresh 요청 시 429 가능.
`codex-multi-auth`는 cooldown 패턴 적용.

## Commits

```
test: add OAuth JWT extraction and session affinity tests
test: verify form-urlencoded token exchange
chore: document E2E verification results
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 130 OAuth Token Collision Fix

> Superseded security note (2026-06-25): This document predates the 280 security patch plan and Phase 10-60 hardening. Treat release-readiness, full-email UI, ordinal request-log labels, unauthenticated management API, fail-open fallback, and earlier account-boundary claims here as historical only. Current merge/deploy evidence is tracked under `devlog/280_codex-multi-auth-security-patch-plan/` and `devlog/_plan/260624_codex-multi-auth-security-implementation/`.

## Problem

Pool account OAuth login can invalidate the main Codex CLI token (`~/.codex/auth.json`).

**Root Cause** (confirmed via openai/codex#10332 + hermes-agent#22903):
- OpenAI uses **single-use refresh token rotation**: each refresh token can only be used once
- When the browser is already logged into ChatGPT as the main account and the user clicks "Add pool account", the OAuth flow auto-completes with the main account's session
- Result: opencodex gets tokens for the SAME account as codex-rs
- When either process refreshes its token, the other's refresh token is permanently invalidated
- codex-rs sees `token_invalidated` on `codex_apps` MCP startup

**Secondary risk**: Even with different accounts, `fetchPoolAccountQuota()` actively refreshes pool tokens via `getValidCodexToken()`, increasing refresh frequency and potential for timing issues.

## Solution

Three-layer defense:

### Layer 1: Force fresh login (prevent same-account collision)
- Add `prompt: "login"` to OAuth authorize params in `chatgpt.ts`
- This forces a fresh login screen instead of auto-consenting with the current session
- User must explicitly choose which account to log in with

### Layer 2: Email collision detection
- After OAuth token exchange, extract email from JWT
- Compare against main account email (from `~/.codex/auth.json` via `fetchMainAccountInfo()`)
- Compare against existing pool account emails
- Reject with clear error if collision detected

### Layer 3: Graceful token failure handling
- When `getValidCodexToken()` fails (revoked/expired), return a typed error instead of throwing
- `fetchPoolAccountQuota()` catches the error and returns `null` (quota bar hidden)
- API response includes `needsReauth: true` flag for failed accounts
- GUI shows "Re-login needed" badge instead of hiding quota silently

## Files

### MODIFY: `src/oauth/chatgpt.ts`
- Add `prompt: "login"` to authorize URL params (line 77-87)

### MODIFY: `src/codex-auth-api.ts`
- Add `checkEmailCollision()` function: reads main email from `fetchMainAccountInfo()`, compares against pool list
- In POST `/api/codex-auth/accounts` handler: after OAuth completes, validate email uniqueness
- In login flow completion (line 193-250): extract email, check collision before saving
- `fetchPoolAccountQuota()`: catch token errors, return null gracefully

### MODIFY: `src/codex-account-store.ts`
- `getValidCodexToken()`: catch refresh failures, return typed error with reason
- Add `isTokenValid(id)` helper that checks without refreshing

### MODIFY: `gui/src/pages/CodexAuth.tsx`
- Handle `needsReauth` flag on pool accounts
- Show "Re-login" badge and re-auth button
- Pool account card shows "Token expired" state instead of empty

## Classification

C3 → C4 care (security-adjacent: auth tokens, session management)
Verification: STANDARD (focused on auth flow correctness)
Loading
Loading