# Programmatic GitHub OAuth Login (requests-based) Full OAuth flow without a browser — for new-api/one-api/gorouter family services. Works up to the 2FA gate. ## Architecture Discovery new-api/one-api forks expose OAuth config via `GET /api/status`: ```json { "github_oauth": true, "github_client_id": "Ov23lipc1Ups6bRqeQYE" } ``` The callback URL is always `{server_address}/oauth/github` (visible in admin panel JS). ## Full Flow ### 1. Construct GitHub OAuth URL ```python import secrets state = secrets.token_hex(16) url = f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&scope=user:email&state={state}" ``` ### 2. GET authorize page → parse login form ```python session = requests.Session() session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }) r = session.get(auth_url, allow_redirects=True, timeout=30) ``` If not already logged into GitHub, redirected to `https://github.com/login?...`. Parse hidden fields: ```python from bs4 import BeautifulSoup soup = BeautifulSoup(r.text, 'html.parser') token = [REDACTED]'input', {'name': 'authenticity_token'}).get('value') # Also needed (GitHub anti-bot): timestamp = re.search(r'name="timestamp"[^>]*value="([^"]+)"', r.text).group(1) timestamp_secret = re.search(r'name="timestamp_secret"[^>]*value="([^"]+)"', r.text).group(1) ``` ### 3. POST login credentials ```python login_data = { 'login': 'USERNAME', # GitHub username, NOT email 'password': 'PASSWORD', 'authenticity_token': token, 'timestamp': timestamp, 'timestamp_secret': timestamp_secret, } r2 = session.post('https://github.com/session', data=login_data, allow_redirects=True) ``` ### 4. Route by redirect target | Final URL contains | Meaning | Action | |---|---|---| | `two-factor` | 2FA required | **STOP** — cannot proceed without user | | `authorize` | Login OK, OAuth consent needed | Submit authorization form (step 5) | | `session` or `login` | Login FAILED | Check for error messages in page | | `gorouter.app` (or target) | Already authorized | Extract session cookies — done | ### 5. Submit OAuth authorization ```python soup2 = BeautifulSoup(r2.text, 'html.parser') auth_token = [REDACTED]'input', {'name': 'authenticity_token'}).get('value') r3 = session.post(r2.url, data={'authenticity_token': auth_token}, allow_redirects=True) if 'gorouter.app' in r3.url: # or target service domain # Session cookies are in session.cookies — use them print(f"Session: {dict(session.cookies)}") ``` ### 6. Use the session ```python # Access protected endpoints with the authenticated session r4 = session.get('https://gorouter.app/api/v1/token?p=1&size=20', headers={'Accept': 'application/json'}) ``` ## Pitfalls ### 2FA is a hard stop GitHub 2FA redirects to `https://github.com/sessions/two-factor/app`. There is NO programmatic bypass. The page may offer "Use recovery code" or "Use security key" — none are automatable. Stop and report to user. ### Username, not email The `login` field in the POST to `/session` takes the GitHub **username** (e.g. `Chunkuotriumph`), NOT the email. Using email may still work but username is canonical. ### Session cookie domain After successful OAuth, the session cookie is set for the **target service domain** (e.g. `gorouter.app`), not `github.com`. Use the same `requests.Session()` throughout the flow. ### Timestamp fields are dynamic `timestamp` and `timestamp_secret` change on every page load. Always parse them fresh — never reuse from a previous attempt. ## When to Use - Validating GitHub credentials against new-api/one-api services - Programmatic login when browser is unavailable - Checking if a GitHub account has 2FA before attempting full browser flow - Batch checking which accounts in a pool can access a service