Spaces:
Paused
Paused
Mirrowel commited on
Commit ·
abbdf2a
1
Parent(s): 302d563
fix(auth): 🔨 tighten oauth refresh intervals, expand refresh buffers, improve logging and add iFlow docs
Browse files- lower default OAUTH_REFRESH_INTERVAL from 3600s to 600s (updated .env.example, README, and background refresher)
- move background refresher sleep to after proactive refresh loop and suppress noisy info log to avoid unnecessary startup delay/noise
- increase REFRESH_EXPIRY_BUFFER_SECONDS for Gemini (30 minutes) and Qwen (3 hours)
- convert several lib_logger.info calls to debug and add richer error logging for HTTP failures and missing refresh tokens
- add Content-Type/Accept headers for Qwen token requests and surface HTTP error bodies for easier troubleshooting
- add extracted documentation: iFlow OAuth flow and iFlow JS bundle summary (docs/iflow_auth_flow_extracted.md, docs/iflow_js_bundle_summary.md)
- .env.example +1 -1
- README.md +2 -2
- docs/iflow_auth_flow_extracted.md +185 -0
- docs/iflow_js_bundle_summary.md +86 -0
- src/rotator_library/background_refresher.py +5 -5
- src/rotator_library/providers/gemini_auth_base.py +3 -3
- src/rotator_library/providers/iflow_auth_base.py +5 -2
- src/rotator_library/providers/qwen_auth_base.py +8 -3
- src/rotator_library/usage_manager.py +1 -1
.env.example
CHANGED
|
@@ -166,7 +166,7 @@ MAX_CONCURRENT_REQUESTS_PER_KEY_IFLOW=1
|
|
| 166 |
# --- OAuth Refresh Interval ---
|
| 167 |
# How often, in seconds, the background refresher should check and refresh
|
| 168 |
# expired OAuth tokens.
|
| 169 |
-
OAUTH_REFRESH_INTERVAL=
|
| 170 |
|
| 171 |
# --- Skip OAuth Initialization ---
|
| 172 |
# Set to "true" to prevent the proxy from performing the interactive OAuth
|
|
|
|
| 166 |
# --- OAuth Refresh Interval ---
|
| 167 |
# How often, in seconds, the background refresher should check and refresh
|
| 168 |
# expired OAuth tokens.
|
| 169 |
+
OAUTH_REFRESH_INTERVAL=600 # Default is 600 seconds (10 minutes)
|
| 170 |
|
| 171 |
# --- Skip OAuth Initialization ---
|
| 172 |
# Set to "true" to prevent the proxy from performing the interactive OAuth
|
README.md
CHANGED
|
@@ -480,9 +480,9 @@ The following advanced settings can be added to your `.env` file (or configured
|
|
| 480 |
|
| 481 |
#### OAuth and Refresh Settings
|
| 482 |
|
| 483 |
-
- **`OAUTH_REFRESH_INTERVAL`**: Controls how often (in seconds) the background refresher checks for expired OAuth tokens. Default is `
|
| 484 |
```env
|
| 485 |
-
OAUTH_REFRESH_INTERVAL=
|
| 486 |
```
|
| 487 |
|
| 488 |
- **`SKIP_OAUTH_INIT_CHECK`**: Set to `true` to skip the interactive OAuth setup/validation check on startup. Essential for non-interactive environments like Docker containers or CI/CD pipelines.
|
|
|
|
| 480 |
|
| 481 |
#### OAuth and Refresh Settings
|
| 482 |
|
| 483 |
+
- **`OAUTH_REFRESH_INTERVAL`**: Controls how often (in seconds) the background refresher checks for expired OAuth tokens. Default is `600` (10 minutes).
|
| 484 |
```env
|
| 485 |
+
OAUTH_REFRESH_INTERVAL=600 # Check every 10 minutes
|
| 486 |
```
|
| 487 |
|
| 488 |
- **`SKIP_OAUTH_INIT_CHECK`**: Set to `true` to skip the interactive OAuth setup/validation check on startup. Essential for non-interactive environments like Docker containers or CI/CD pipelines.
|
docs/iflow_auth_flow_extracted.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
**iFlow OAuth Flow – Extracted From Bundle (`iflow.js`)**
|
| 2 |
+
|
| 3 |
+
Purpose
|
| 4 |
+
- Provide a precise, implementation-ready description of the iFlow authentication flow as embedded in the bundled CLI (`external/iflow-cli-tarball/package/bundle/iflow.js`).
|
| 5 |
+
- Enable faithful replication in other tooling without re-reading the minified bundle.
|
| 6 |
+
|
| 7 |
+
Scope
|
| 8 |
+
- Focuses ONLY on iFlow-specific authorization (NOT generic Google / other OAuth client code also present in the bundle).
|
| 9 |
+
- Omits internal unrelated libraries (telemetry, localization, unrelated OAuth handlers for other issuers).
|
| 10 |
+
|
| 11 |
+
Overview of Sequence
|
| 12 |
+
1. Determine callback port: Either `OAUTH_CALLBACK_PORT` env var, or dynamically allocate an ephemeral free port.
|
| 13 |
+
2. Build local redirect URI: `http://localhost:{port}/oauth2callback` (host override via `OAUTH_CALLBACK_HOST`, default `localhost`).
|
| 14 |
+
3. Generate anti-CSRF `state`: 32 random hex bytes.
|
| 15 |
+
4. Construct authorization URL:
|
| 16 |
+
- Base: `https://iflow.cn/oauth`
|
| 17 |
+
- Query parameters:
|
| 18 |
+
- `loginMethod=phone`
|
| 19 |
+
- `type=phone`
|
| 20 |
+
- `redirect=<encoded redirect URL>&state=<state>` (note: the bundle concatenates and encodes the redirect first, then appends `&state=` and the state value; effectively `redirect` holds both the actual callback URL and embeds the state parameter)
|
| 21 |
+
- `client_id=10009311001`
|
| 22 |
+
5. Display URL & attempt to open it in the system browser.
|
| 23 |
+
6. Local HTTP server listens for requests to `/oauth2callback`.
|
| 24 |
+
7. On callback:
|
| 25 |
+
- Reject if path not `/oauth2callback`.
|
| 26 |
+
- If `error` param present: redirect to `https://iflow.cn/oauth/error` and fail.
|
| 27 |
+
- If `state` mismatch: respond with plain text error (“State mismatch. Possible CSRF attack”).
|
| 28 |
+
- If `code` is present and `state` matches: exchange code for tokens.
|
| 29 |
+
8. Token exchange: POST `https://iflow.cn/oauth/token`.
|
| 30 |
+
9. On success: persist credentials (JSON file via internal helper), then fetch user info to obtain `apiKey`.
|
| 31 |
+
10. Redirect browser to success page: `https://iflow.cn/oauth/success`.
|
| 32 |
+
|
| 33 |
+
Authorization URL Details
|
| 34 |
+
- Final composed form (illustrative – values substituted):
|
| 35 |
+
```
|
| 36 |
+
https://iflow.cn/oauth?loginMethod=phone&type=phone&redirect={ENCODED_REDIRECT_AND_STATE}&client_id=10009311001
|
| 37 |
+
```
|
| 38 |
+
- Important nuance: The bundle embeds the state inside the `redirect` query value (pattern: `redirect=<encodeURIComponent(callbackUrl)> &state=<state>` BEFORE concatenation). Replication MAY instead send state as a separate top-level query parameter if the server tolerates it; however to remain faithful, mimic the bundle pattern: keep `state` appended inside the `redirect` value.
|
| 39 |
+
- Conservative replication: EXACT concatenation used by bundle:
|
| 40 |
+
- Let `callback = http://localhost:{port}/oauth2callback`
|
| 41 |
+
- Let `state = <random-hex>`
|
| 42 |
+
- Let `redirectParamValue = encodeURIComponent(callback) + "&state=" + state`
|
| 43 |
+
- Query param: `redirect=redirectParamValue`
|
| 44 |
+
- Full URL: `https://iflow.cn/oauth?loginMethod=phone&type=phone&redirect=${redirectParamValue}&client_id=10009311001`
|
| 45 |
+
|
| 46 |
+
Callback Handling Logic
|
| 47 |
+
- Expected parameters (in parsed search params of original request URL):
|
| 48 |
+
- `code`: authorization code (required for success)
|
| 49 |
+
- `state`: must equal originally generated random hex string
|
| 50 |
+
- `error`: signals authentication failure
|
| 51 |
+
- Error conditions:
|
| 52 |
+
- Missing `code`: treated as failure ("noCodeFound").
|
| 53 |
+
- Missing or mismatched `state`: potential CSRF → failure.
|
| 54 |
+
- Any `error` param → failure.
|
| 55 |
+
- On success: code progresses to token exchange.
|
| 56 |
+
|
| 57 |
+
Token Exchange (Authorization Code Grant)
|
| 58 |
+
- Endpoint: `https://iflow.cn/oauth/token`
|
| 59 |
+
- Method: `POST`
|
| 60 |
+
- Headers:
|
| 61 |
+
- `Content-Type: application/x-www-form-urlencoded`
|
| 62 |
+
- `Authorization: Basic <base64(client_id:client_secret)>`
|
| 63 |
+
- Body parameters (URL-encoded form):
|
| 64 |
+
- `grant_type=authorization_code`
|
| 65 |
+
- `code=<authorization_code>`
|
| 66 |
+
- `redirect_uri=http://localhost:{port}/oauth2callback` (MUST match original callback URI before encoding inside redirect param)
|
| 67 |
+
- `client_id=10009311001`
|
| 68 |
+
- `client_secret=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW`
|
| 69 |
+
- Response JSON fields consumed:
|
| 70 |
+
- `access_token`
|
| 71 |
+
- `refresh_token`
|
| 72 |
+
- `expires_in` (converted to absolute expiry timestamp)
|
| 73 |
+
- `token_type`
|
| 74 |
+
- `scope`
|
| 75 |
+
|
| 76 |
+
User Info / API Key Retrieval
|
| 77 |
+
- Endpoint: `https://iflow.cn/api/oauth/getUserInfo?accessToken=<access_token>`
|
| 78 |
+
- Method: `GET`
|
| 79 |
+
- Response expected shape (simplified):
|
| 80 |
+
```json
|
| 81 |
+
{
|
| 82 |
+
"success": true,
|
| 83 |
+
"data": { "apiKey": "...", "email": "..." }
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
- On success, the extracted `apiKey` is appended to stored credentials and cached.
|
| 87 |
+
- Retries: The bundle performs up to 3 retries with backoff for transient (5xx / 408 / 429) failures.
|
| 88 |
+
|
| 89 |
+
Persisted Credential Structure (conceptual)
|
| 90 |
+
- Keys stored: `access_token`, `refresh_token`, `expiry_date`, `token_type`, `scope`, optionally `apiKey`.
|
| 91 |
+
- File path: derived via internal helper (not exposed here); replication may choose its own path (e.g. `~/.iflow/credentials.json`).
|
| 92 |
+
- Permissions set mode `0600` (decimal 384) when writing.
|
| 93 |
+
|
| 94 |
+
Refresh Flow
|
| 95 |
+
- Trigger conditions:
|
| 96 |
+
- Token nearing expiry (< 24 hours) or explicit refresh check in background tasks.
|
| 97 |
+
- Refresh request:
|
| 98 |
+
- POST `https://iflow.cn/oauth/token`
|
| 99 |
+
- Headers: same as authorization-code exchange.
|
| 100 |
+
- Body params:
|
| 101 |
+
- `grant_type=refresh_token`
|
| 102 |
+
- `refresh_token=<stored_refresh_token>`
|
| 103 |
+
- `client_id=10009311001`
|
| 104 |
+
- `client_secret=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW`
|
| 105 |
+
- Response handling mirrors initial token response (update expiry, access/refresh tokens, scope, token type). May re-fetch user info to ensure `apiKey` is current.
|
| 106 |
+
|
| 107 |
+
PKCE Status
|
| 108 |
+
- The bundle contains a generic OAuth library with PKCE helpers (code_verifier / code_challenge generation) for other issuers.
|
| 109 |
+
- The iFlow-specific code path DOES NOT include `code_challenge` or `code_verifier` in its authorization or token requests.
|
| 110 |
+
- Conclusion: PKCE is NOT required for the current iFlow CLI flow; replication should omit PKCE unless the provider later enforces it.
|
| 111 |
+
|
| 112 |
+
Environment Variables Influencing Flow
|
| 113 |
+
- `OAUTH_CALLBACK_PORT`: If set and valid (1–65535), use as callback port instead of ephemeral port.
|
| 114 |
+
- `OAUTH_CALLBACK_HOST`: Hostname for listening; default `localhost`.
|
| 115 |
+
- (Potential others exist for generic flows, but only these are critical for iFlow-specific path.)
|
| 116 |
+
|
| 117 |
+
Security Considerations
|
| 118 |
+
- State validation prevents CSRF / unsolicited redirects.
|
| 119 |
+
- The unusual embedding of state inside the `redirect` parameter means the state also appears server-side only if parsed out; exact replication should preserve this pattern unless confirmed safe to normalize.
|
| 120 |
+
- Basic auth header exposes client_secret; always use HTTPS (the CLI does). Do not log raw Authorization headers.
|
| 121 |
+
- Persisted credentials should be stored with restrictive file permissions.
|
| 122 |
+
- Refresh token invalidation must trigger re-authentication (bundle clears credentials on failure).
|
| 123 |
+
|
| 124 |
+
Error Handling Patterns (Simplified)
|
| 125 |
+
- Authorization errors: Redirect user to error page `https://iflow.cn/oauth/error`.
|
| 126 |
+
- Token request failure: Inspect HTTP status & status text; raise user-facing error.
|
| 127 |
+
- Refresh failure or expiry: Clear credential file and require re-login.
|
| 128 |
+
- User info failures: Retry transient errors (5xx / 408 / 429), otherwise log and continue without apiKey.
|
| 129 |
+
|
| 130 |
+
Replication Checklist
|
| 131 |
+
1. Generate random hex `state` (32 bytes recommended).
|
| 132 |
+
2. Select callback port: env override or ephemeral free port.
|
| 133 |
+
3. Build callback URL `http://localhost:{port}/oauth2callback`.
|
| 134 |
+
4. Compose authorization URL exactly as bundle does (embedded `&state=` inside `redirect` value).
|
| 135 |
+
5. Launch local HTTP server; validate `state`, extract `code`.
|
| 136 |
+
6. Exchange code using Basic auth + form body (include both client_id/client_secret).
|
| 137 |
+
7. Store credentials; compute `expiry_date = now + expires_in*1000`.
|
| 138 |
+
8. Fetch user info to get `apiKey`; attach to stored credentials.
|
| 139 |
+
9. Implement refresh POST (grant_type=refresh_token) with same auth style.
|
| 140 |
+
10. Implement retry & expiry logic; clear credentials if permanently invalid.
|
| 141 |
+
11. Never include PKCE unless provider changes requirements.
|
| 142 |
+
12. Optionally open browser automatically and present fallback URL if open fails.
|
| 143 |
+
|
| 144 |
+
Minimal Pseudocode Outline (Language-Agnostic)
|
| 145 |
+
```text
|
| 146 |
+
state = randomHex(32)
|
| 147 |
+
port = env.OAUTH_CALLBACK_PORT || findFreePort()
|
| 148 |
+
callback = "http://localhost:" + port + "/oauth2callback"
|
| 149 |
+
redirectParamValue = urlencode(callback) + "&state=" + state
|
| 150 |
+
authUrl = "https://iflow.cn/oauth?loginMethod=phone&type=phone&redirect=" + redirectParamValue + "&client_id=10009311001"
|
| 151 |
+
|
| 152 |
+
openBrowser(authUrl)
|
| 153 |
+
code = await waitForCallback(callback, state)
|
| 154 |
+
|
| 155 |
+
tokenResp = POST https://iflow.cn/oauth/token
|
| 156 |
+
Headers: Content-Type, Authorization: Basic base64(client_id:client_secret)
|
| 157 |
+
Body (form): grant_type=authorization_code, code, redirect_uri=callback, client_id, client_secret
|
| 158 |
+
|
| 159 |
+
creds = {access_token, refresh_token, expiry_date=now+expires_in*1000, token_type, scope}
|
| 160 |
+
userInfo = GET https://iflow.cn/api/oauth/getUserInfo?accessToken=creds.access_token
|
| 161 |
+
if userInfo.success: creds.apiKey = userInfo.data.apiKey
|
| 162 |
+
store(creds)
|
| 163 |
+
|
| 164 |
+
// Refresh flow
|
| 165 |
+
if nearing expiry:
|
| 166 |
+
refreshResp = POST https://iflow.cn/oauth/token (grant_type=refresh_token,...)
|
| 167 |
+
update creds, optionally re-fetch apiKey
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
Testing Recommendations
|
| 171 |
+
- Simulate callback manually with crafted URL containing correct `code` & `state` to test server path.
|
| 172 |
+
- Validate rejection on mismatched state.
|
| 173 |
+
- Confirm `apiKey` presence after user info call.
|
| 174 |
+
- Force refresh by adjusting expiry_date to near past and invoking refresh routine.
|
| 175 |
+
|
| 176 |
+
Future-Proofing
|
| 177 |
+
- Monitor provider announcements for PKCE enforcement; if required, switch to `code_challenge` / `code_verifier` standard pattern.
|
| 178 |
+
- Consider switching state embedding to separate query parameter if provider standardizes (requires test).
|
| 179 |
+
|
| 180 |
+
Summary
|
| 181 |
+
- The iFlow CLI uses a straightforward OAuth2 Authorization Code Grant with Basic client authentication and NO PKCE.
|
| 182 |
+
- The distinctive element is embedding `state` within the `redirect` parameter value.
|
| 183 |
+
- Replication requires careful reconstruction of the authorization URL, robust callback validation, token + refresh exchanges, and an additional user info call to retrieve the operational `apiKey`.
|
| 184 |
+
|
| 185 |
+
-- End of extracted flow documentation
|
docs/iflow_js_bundle_summary.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
**iFlow JS Bundle Summary**
|
| 2 |
+
|
| 3 |
+
Path: `external/iflow-cli-tarball/package/bundle/iflow.js`
|
| 4 |
+
|
| 5 |
+
Purpose
|
| 6 |
+
- A single-file bundled/minified Node/Browser CLI distribution for the iFlow CLI.
|
| 7 |
+
- Contains the CLI runtime, UI strings, crypto helpers, networking logic and an OAuth-based authentication flow used by iFlow's tooling.
|
| 8 |
+
|
| 9 |
+
Bundle metadata
|
| 10 |
+
- File size (approx): ~10.2 MB (minified/bundled, many libraries included).
|
| 11 |
+
- Other files in same folder: `iflow-cli-vscode-ide-companion-*.vsix`, `sandbox-*.sb` files (IDE companions / sandbox artifacts).
|
| 12 |
+
|
| 13 |
+
High-level contents and useful targets
|
| 14 |
+
- Crypto helpers
|
| 15 |
+
- Implements both browser and Node crypto adapters (named `BrowserCrypto` and `NodeCrypto`).
|
| 16 |
+
- Exposed helpers include: `sha256DigestHex`, `sha256DigestBase64`, random bytes generator, HMAC signing helpers, and base64 helpers.
|
| 17 |
+
- These utilities are used to compute PKCE code_challenge values (S256) and other digests.
|
| 18 |
+
|
| 19 |
+
- PKCE evidence
|
| 20 |
+
- The bundle includes code that computes SHA-256 digests and derives hex/base64 strings. This is strong evidence PKCE is used by the CLI.
|
| 21 |
+
- Keywords to look for: `sha256DigestHex`, `code_challenge`, `code_verifier`, `code_challenge_method`, `PKCE`, `S256`.
|
| 22 |
+
|
| 23 |
+
- OAuth + Browser flow
|
| 24 |
+
- The CLI appears to use a browser-based authorization flow:
|
| 25 |
+
- It constructs an authorization URL (authUrl) and attempts to open it in the user's browser.
|
| 26 |
+
- It starts or expects a local callback path (the pattern `/oauth2callback` appears in the bundle).
|
| 27 |
+
- After the browser redirects to the local callback, the bundle's callback handler extracts `code` and `state` and resolves them (pattern seen as resolving an object like `{code: c, state: u}`).
|
| 28 |
+
- User-facing strings found in the bundle include messages and instructions such as:
|
| 29 |
+
- "Attempting to open authentication page in your browser."
|
| 30 |
+
- "Navigate to the URL below to sign in" (or similar localized messages).
|
| 31 |
+
|
| 32 |
+
- Token exchange and API-key retrieval
|
| 33 |
+
- The bundle contains logic that exchanges an authorization code for tokens and then retrieves user info to extract provider-specific `apiKey` (iFlow uses a separate API key for API calls).
|
| 34 |
+
- Token exchange patterns include POSTing to token endpoints and handling success/error JSON responses.
|
| 35 |
+
- Note: In some CLI flows, both PKCE and client-secret based exchanges may be present; inspect the exact token-request payload to confirm which parameters are sent.
|
| 36 |
+
|
| 37 |
+
- Additional artifacts bundled
|
| 38 |
+
- Localization strings, telemetry/logging code, UI wrappers, and other third-party libraries are bundled together, making raw inspection noisy.
|
| 39 |
+
|
| 40 |
+
Practical guidance for other agents
|
| 41 |
+
- Why it's tricky
|
| 42 |
+
- `iflow.js` is minified/compiled and contains many third-party modules concatenated together. This makes it hard to read directly—searching for specific keywords is the fastest approach.
|
| 43 |
+
|
| 44 |
+
- Key search terms (recommended)
|
| 45 |
+
- `sha256`, `sha256DigestHex`, `code_challenge`, `code_verifier`, `code_challenge_method`, `authUrl`, `oauth2callback`, `Attempting to open`, `authUrl`, `authorize`, `token`, `getUserInfo`, `apiKey`, `redirect`, `state`.
|
| 46 |
+
|
| 47 |
+
- Useful PowerShell commands (run at repo root) to inspect the bundle quickly
|
| 48 |
+
- Search for keywords:
|
| 49 |
+
```powershell
|
| 50 |
+
Select-String -Path external\iflow-cli-tarball\package\bundle\iflow.js -Pattern "sha256|code_challenge|authUrl|oauth2callback|getUserInfo|apiKey|Attempting to open" -SimpleMatch
|
| 51 |
+
```
|
| 52 |
+
- View a nearby window of lines (example indices found via Select-String):
|
| 53 |
+
```powershell
|
| 54 |
+
# read lines 940..1005 (example offsets)
|
| 55 |
+
(Get-Content -Path 'external\iflow-cli-tarball\package\bundle\iflow.js')[940..1005] -join "`n"
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
- How to extract provider-specific values
|
| 59 |
+
1. Search for `client_id`, `authorize`, `oauth` and `token` strings to find endpoint constants and client identifiers.
|
| 60 |
+
2. Look for `authUrl` construction code to see which query parameters are included (e.g., `redirect`, `state`, `client_id`, `code_challenge`).
|
| 61 |
+
3. Inspect token exchange code to see whether it sends `client_secret` or `code_verifier` (or both).
|
| 62 |
+
|
| 63 |
+
- Repro steps for someone implementing the auth flow in another language (concise)
|
| 64 |
+
1. Locate the `authUrl` construction and confirm required query params (client_id, redirect, state, any `loginMethod`/`type` parameters).
|
| 65 |
+
2. Confirm whether PKCE is required by checking for `code_challenge`/`code_challenge_method` in the auth request and `code_verifier` in the token request.
|
| 66 |
+
3. Confirm redirect URI path (`/oauth2callback` pattern) and port if the CLI starts a local callback server.
|
| 67 |
+
4. Confirm token exchange method and auth method (Basic auth with client_id:client_secret in Authorization header OR an exchange that uses client_secret in body, or an exchange that uses `code_verifier` and omits client_secret).
|
| 68 |
+
5. Confirm post-token user info fetch that returns `apiKey` separately—recreate that call to obtain the API key.
|
| 69 |
+
|
| 70 |
+
- Recommendations / next actions for an agent
|
| 71 |
+
- If you need to replicate exactly, extract the authUrl and token request payloads verbatim from the bundle.
|
| 72 |
+
- If the bundle uses PKCE, add PKCE (code_verifier + code_challenge=S256) to your client implementation. If it uses Basic client_secret authentication instead, ensure you include the client_secret in exchanges.
|
| 73 |
+
- Use a modular approach: implement the browser + callback pattern, but make the PKCE piece optional/conditional so it can work with servers that require or permit it.
|
| 74 |
+
|
| 75 |
+
- References inside this repo (helpful snippets to reuse)
|
| 76 |
+
- Example PKCE helper exists in `src/rotator_library/providers/qwen_auth_base.py` — you can reuse code-verifier / code-challenge generation from there.
|
| 77 |
+
|
| 78 |
+
Appendix: quick checklist for auditors
|
| 79 |
+
- [ ] Confirm `authUrl` query parameters.
|
| 80 |
+
- [ ] Confirm presence of `code_challenge` in auth request.
|
| 81 |
+
- [ ] Confirm presence of `code_verifier` in token request.
|
| 82 |
+
- [ ] Confirm token exchange auth method (Basic, client_secret body param, or neither).
|
| 83 |
+
- [ ] Confirm local callback path and default port.
|
| 84 |
+
- [ ] Confirm user-info endpoint that returns `apiKey`.
|
| 85 |
+
|
| 86 |
+
-- End of summary
|
src/rotator_library/background_refresher.py
CHANGED
|
@@ -17,11 +17,11 @@ class BackgroundRefresher:
|
|
| 17 |
"""
|
| 18 |
def __init__(self, client: 'RotatingClient'):
|
| 19 |
try:
|
| 20 |
-
interval_str = os.getenv("OAUTH_REFRESH_INTERVAL", "
|
| 21 |
self._interval = int(interval_str)
|
| 22 |
except ValueError:
|
| 23 |
-
lib_logger.warning(f"Invalid OAUTH_REFRESH_INTERVAL '{interval_str}'. Falling back to
|
| 24 |
-
self._interval =
|
| 25 |
self._client = client
|
| 26 |
self._task: Optional[asyncio.Task] = None
|
| 27 |
|
|
@@ -46,8 +46,7 @@ class BackgroundRefresher:
|
|
| 46 |
"""The main loop for the background task."""
|
| 47 |
while True:
|
| 48 |
try:
|
| 49 |
-
|
| 50 |
-
lib_logger.info("Running proactive token refresh check...")
|
| 51 |
|
| 52 |
oauth_configs = self._client.get_oauth_credentials()
|
| 53 |
for provider, paths in oauth_configs.items():
|
|
@@ -58,6 +57,7 @@ class BackgroundRefresher:
|
|
| 58 |
await provider_plugin.proactively_refresh(path)
|
| 59 |
except Exception as e:
|
| 60 |
lib_logger.error(f"Error during proactive refresh for '{path}': {e}")
|
|
|
|
| 61 |
except asyncio.CancelledError:
|
| 62 |
break
|
| 63 |
except Exception as e:
|
|
|
|
| 17 |
"""
|
| 18 |
def __init__(self, client: 'RotatingClient'):
|
| 19 |
try:
|
| 20 |
+
interval_str = os.getenv("OAUTH_REFRESH_INTERVAL", "600")
|
| 21 |
self._interval = int(interval_str)
|
| 22 |
except ValueError:
|
| 23 |
+
lib_logger.warning(f"Invalid OAUTH_REFRESH_INTERVAL '{interval_str}'. Falling back to 600s.")
|
| 24 |
+
self._interval = 600
|
| 25 |
self._client = client
|
| 26 |
self._task: Optional[asyncio.Task] = None
|
| 27 |
|
|
|
|
| 46 |
"""The main loop for the background task."""
|
| 47 |
while True:
|
| 48 |
try:
|
| 49 |
+
#lib_logger.info("Running proactive token refresh check...")
|
|
|
|
| 50 |
|
| 51 |
oauth_configs = self._client.get_oauth_credentials()
|
| 52 |
for provider, paths in oauth_configs.items():
|
|
|
|
| 57 |
await provider_plugin.proactively_refresh(path)
|
| 58 |
except Exception as e:
|
| 59 |
lib_logger.error(f"Error during proactive refresh for '{path}': {e}")
|
| 60 |
+
await asyncio.sleep(self._interval)
|
| 61 |
except asyncio.CancelledError:
|
| 62 |
break
|
| 63 |
except Exception as e:
|
src/rotator_library/providers/gemini_auth_base.py
CHANGED
|
@@ -23,7 +23,7 @@ CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleuserconten
|
|
| 23 |
CLIENT_SECRET = "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" #https://api.kilocode.ai/extension-config.json
|
| 24 |
TOKEN_URI = "https://oauth2.googleapis.com/token"
|
| 25 |
USER_INFO_URI = "https://www.googleapis.com/oauth2/v1/userinfo"
|
| 26 |
-
REFRESH_EXPIRY_BUFFER_SECONDS =
|
| 27 |
|
| 28 |
console = Console()
|
| 29 |
|
|
@@ -192,7 +192,7 @@ class GeminiAuthBase:
|
|
| 192 |
if not force and not self._is_token_expired(self._credentials_cache.get(path, creds)):
|
| 193 |
return self._credentials_cache.get(path, creds)
|
| 194 |
|
| 195 |
-
lib_logger.
|
| 196 |
refresh_token = creds.get("refresh_token")
|
| 197 |
if not refresh_token:
|
| 198 |
raise ValueError("No refresh_token found in credentials file.")
|
|
@@ -317,7 +317,7 @@ class GeminiAuthBase:
|
|
| 317 |
# But log it for debugging purposes
|
| 318 |
|
| 319 |
await self._save_credentials(path, creds)
|
| 320 |
-
lib_logger.
|
| 321 |
return creds
|
| 322 |
|
| 323 |
async def proactively_refresh(self, credential_path: str):
|
|
|
|
| 23 |
CLIENT_SECRET = "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" #https://api.kilocode.ai/extension-config.json
|
| 24 |
TOKEN_URI = "https://oauth2.googleapis.com/token"
|
| 25 |
USER_INFO_URI = "https://www.googleapis.com/oauth2/v1/userinfo"
|
| 26 |
+
REFRESH_EXPIRY_BUFFER_SECONDS = 30 * 60 # 30 minutes buffer before expiry
|
| 27 |
|
| 28 |
console = Console()
|
| 29 |
|
|
|
|
| 192 |
if not force and not self._is_token_expired(self._credentials_cache.get(path, creds)):
|
| 193 |
return self._credentials_cache.get(path, creds)
|
| 194 |
|
| 195 |
+
lib_logger.debug(f"Refreshing Gemini OAuth token for '{Path(path).name}' (forced: {force})...")
|
| 196 |
refresh_token = creds.get("refresh_token")
|
| 197 |
if not refresh_token:
|
| 198 |
raise ValueError("No refresh_token found in credentials file.")
|
|
|
|
| 317 |
# But log it for debugging purposes
|
| 318 |
|
| 319 |
await self._save_credentials(path, creds)
|
| 320 |
+
lib_logger.debug(f"Successfully refreshed Gemini OAuth token for '{Path(path).name}'.")
|
| 321 |
return creds
|
| 322 |
|
| 323 |
async def proactively_refresh(self, credential_path: str):
|
src/rotator_library/providers/iflow_auth_base.py
CHANGED
|
@@ -414,7 +414,7 @@ class IFlowAuthBase:
|
|
| 414 |
|
| 415 |
creds_from_file = self._credentials_cache[path]
|
| 416 |
|
| 417 |
-
lib_logger.
|
| 418 |
refresh_token = creds_from_file.get("refresh_token")
|
| 419 |
if not refresh_token:
|
| 420 |
raise ValueError("No refresh_token found in iFlow credentials file.")
|
|
@@ -452,6 +452,9 @@ class IFlowAuthBase:
|
|
| 452 |
except httpx.HTTPStatusError as e:
|
| 453 |
last_error = e
|
| 454 |
status_code = e.response.status_code
|
|
|
|
|
|
|
|
|
|
| 455 |
|
| 456 |
# [STATUS CODE HANDLING]
|
| 457 |
if status_code in (401, 403):
|
|
@@ -522,7 +525,7 @@ class IFlowAuthBase:
|
|
| 522 |
creds_from_file["_proxy_metadata"]["last_check_timestamp"] = time.time()
|
| 523 |
|
| 524 |
await self._save_credentials(path, creds_from_file)
|
| 525 |
-
lib_logger.
|
| 526 |
return creds_from_file
|
| 527 |
|
| 528 |
async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
|
|
|
|
| 414 |
|
| 415 |
creds_from_file = self._credentials_cache[path]
|
| 416 |
|
| 417 |
+
lib_logger.debug(f"Refreshing iFlow OAuth token for '{Path(path).name}'...")
|
| 418 |
refresh_token = creds_from_file.get("refresh_token")
|
| 419 |
if not refresh_token:
|
| 420 |
raise ValueError("No refresh_token found in iFlow credentials file.")
|
|
|
|
| 452 |
except httpx.HTTPStatusError as e:
|
| 453 |
last_error = e
|
| 454 |
status_code = e.response.status_code
|
| 455 |
+
error_body = e.response.text
|
| 456 |
+
|
| 457 |
+
lib_logger.error(f"[REFRESH HTTP ERROR] HTTP {status_code} for '{Path(path).name}': {error_body}")
|
| 458 |
|
| 459 |
# [STATUS CODE HANDLING]
|
| 460 |
if status_code in (401, 403):
|
|
|
|
| 525 |
creds_from_file["_proxy_metadata"]["last_check_timestamp"] = time.time()
|
| 526 |
|
| 527 |
await self._save_credentials(path, creds_from_file)
|
| 528 |
+
lib_logger.debug(f"Successfully refreshed iFlow OAuth token for '{Path(path).name}'.")
|
| 529 |
return creds_from_file
|
| 530 |
|
| 531 |
async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
|
src/rotator_library/providers/qwen_auth_base.py
CHANGED
|
@@ -25,7 +25,7 @@ lib_logger = logging.getLogger('rotator_library')
|
|
| 25 |
CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56" #https://api.kilocode.ai/extension-config.json
|
| 26 |
SCOPE = "openid profile email model.completion"
|
| 27 |
TOKEN_ENDPOINT = "https://chat.qwen.ai/api/v1/oauth2/token"
|
| 28 |
-
REFRESH_EXPIRY_BUFFER_SECONDS =
|
| 29 |
|
| 30 |
console = Console()
|
| 31 |
|
|
@@ -186,9 +186,10 @@ class QwenAuthBase:
|
|
| 186 |
|
| 187 |
creds_from_file = self._credentials_cache[path]
|
| 188 |
|
| 189 |
-
lib_logger.
|
| 190 |
refresh_token = creds_from_file.get("refresh_token")
|
| 191 |
if not refresh_token:
|
|
|
|
| 192 |
raise ValueError("No refresh_token found in Qwen credentials file.")
|
| 193 |
|
| 194 |
# [RETRY LOGIC] Implement exponential backoff for transient errors
|
|
@@ -197,6 +198,8 @@ class QwenAuthBase:
|
|
| 197 |
last_error = None
|
| 198 |
|
| 199 |
headers = {
|
|
|
|
|
|
|
| 200 |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
| 201 |
}
|
| 202 |
|
|
@@ -215,6 +218,8 @@ class QwenAuthBase:
|
|
| 215 |
except httpx.HTTPStatusError as e:
|
| 216 |
last_error = e
|
| 217 |
status_code = e.response.status_code
|
|
|
|
|
|
|
| 218 |
|
| 219 |
# [STATUS CODE HANDLING]
|
| 220 |
if status_code in (401, 403):
|
|
@@ -265,7 +270,7 @@ class QwenAuthBase:
|
|
| 265 |
creds_from_file["_proxy_metadata"]["last_check_timestamp"] = time.time()
|
| 266 |
|
| 267 |
await self._save_credentials(path, creds_from_file)
|
| 268 |
-
lib_logger.
|
| 269 |
return creds_from_file
|
| 270 |
|
| 271 |
async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
|
|
|
|
| 25 |
CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56" #https://api.kilocode.ai/extension-config.json
|
| 26 |
SCOPE = "openid profile email model.completion"
|
| 27 |
TOKEN_ENDPOINT = "https://chat.qwen.ai/api/v1/oauth2/token"
|
| 28 |
+
REFRESH_EXPIRY_BUFFER_SECONDS = 3 * 60 * 60 # 3 hours buffer before expiry
|
| 29 |
|
| 30 |
console = Console()
|
| 31 |
|
|
|
|
| 186 |
|
| 187 |
creds_from_file = self._credentials_cache[path]
|
| 188 |
|
| 189 |
+
lib_logger.debug(f"Refreshing Qwen OAuth token for '{Path(path).name}'...")
|
| 190 |
refresh_token = creds_from_file.get("refresh_token")
|
| 191 |
if not refresh_token:
|
| 192 |
+
lib_logger.error(f"No refresh_token found in '{Path(path).name}'")
|
| 193 |
raise ValueError("No refresh_token found in Qwen credentials file.")
|
| 194 |
|
| 195 |
# [RETRY LOGIC] Implement exponential backoff for transient errors
|
|
|
|
| 198 |
last_error = None
|
| 199 |
|
| 200 |
headers = {
|
| 201 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 202 |
+
"Accept": "application/json",
|
| 203 |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
| 204 |
}
|
| 205 |
|
|
|
|
| 218 |
except httpx.HTTPStatusError as e:
|
| 219 |
last_error = e
|
| 220 |
status_code = e.response.status_code
|
| 221 |
+
error_body = e.response.text
|
| 222 |
+
lib_logger.error(f"HTTP {status_code} for '{Path(path).name}': {error_body}")
|
| 223 |
|
| 224 |
# [STATUS CODE HANDLING]
|
| 225 |
if status_code in (401, 403):
|
|
|
|
| 270 |
creds_from_file["_proxy_metadata"]["last_check_timestamp"] = time.time()
|
| 271 |
|
| 272 |
await self._save_credentials(path, creds_from_file)
|
| 273 |
+
lib_logger.debug(f"Successfully refreshed Qwen OAuth token for '{Path(path).name}'.")
|
| 274 |
return creds_from_file
|
| 275 |
|
| 276 |
async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
|
src/rotator_library/usage_manager.py
CHANGED
|
@@ -105,7 +105,7 @@ class UsageManager:
|
|
| 105 |
last_reset_dt is None
|
| 106 |
or last_reset_dt < reset_threshold_today <= now_utc
|
| 107 |
):
|
| 108 |
-
lib_logger.
|
| 109 |
needs_saving = True
|
| 110 |
|
| 111 |
# Reset cooldowns
|
|
|
|
| 105 |
last_reset_dt is None
|
| 106 |
or last_reset_dt < reset_threshold_today <= now_utc
|
| 107 |
):
|
| 108 |
+
lib_logger.debug(f"Performing daily reset for key ...{key[-6:]}")
|
| 109 |
needs_saving = True
|
| 110 |
|
| 111 |
# Reset cooldowns
|