|
6 | 6 | Frontend never sees the GitHub access token. |
7 | 7 | """ |
8 | 8 | import os |
| 9 | +import re |
9 | 10 | import secrets |
10 | 11 | import httpx |
11 | 12 | from fastapi import APIRouter, HTTPException, Depends, Query |
@@ -205,9 +206,23 @@ async def github_oauth_callback( |
205 | 206 | if not GITHUB_CLIENT_ID or not GITHUB_CLIENT_SECRET: |
206 | 207 | raise HTTPException(status_code=500, detail="GitHub OAuth not configured") |
207 | 208 |
|
208 | | - # Verify state contains user_id (basic CSRF protection) |
209 | | - if not request.state.startswith(auth.user_id): |
210 | | - raise HTTPException(status_code=400, detail="Invalid state parameter") |
| 209 | + # Verify state format and user_id match |
| 210 | + # State format: user_id:random_token (where random_token is base64url from token_urlsafe) |
| 211 | + state_parts = request.state.split(":", 1) |
| 212 | + if len(state_parts) != 2: |
| 213 | + raise HTTPException(status_code=400, detail="Invalid state format") |
| 214 | + |
| 215 | + state_user_id, state_token = state_parts |
| 216 | + if state_user_id != auth.user_id: |
| 217 | + raise HTTPException(status_code=400, detail="State user mismatch") |
| 218 | + |
| 219 | + # Validate token portion: token_urlsafe(32) produces 43 chars of URL-safe base64 |
| 220 | + if len(state_token) != 43: |
| 221 | + raise HTTPException(status_code=400, detail="Invalid state token length") |
| 222 | + |
| 223 | + # Validate charset (URL-safe base64: A-Z, a-z, 0-9, -, _) |
| 224 | + if not re.match(r'^[A-Za-z0-9_-]+$', state_token): |
| 225 | + raise HTTPException(status_code=400, detail="Invalid state token charset") |
211 | 226 |
|
212 | 227 | # Exchange code for access token |
213 | 228 | async with httpx.AsyncClient() as client: |
|
0 commit comments