3.9 KiB
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:
{
"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
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
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:
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
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
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
# 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