Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Commit aacbfaa

Browse files
authored
Merge pull request #138 from XortexAI/ishaan/dev
implement OAuth 2.0 for mcp
2 parents 80c5fc1 + 5dcbb05 commit aacbfaa

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

‎src/api/routes/auth.py‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,36 @@ def _get_and_invalidate_mcp_token(token: str) -> Optional[str]:
7676
return user_id
7777

7878

79+
# ═══════════════════════════════════════════════════════════════════════════
80+
# Standard OAuth 2.0 Store (for ChatGPT UI)
81+
# ═══════════════════════════════════════════════════════════════════════════
82+
_oauth_auth_codes: Dict[str, Dict[str, Any]] = {}
83+
84+
def _generate_auth_code(user_id: str) -> str:
85+
"""Generate a standard OAuth 2.0 authorization code."""
86+
alphabet = string.ascii_letters + string.digits
87+
code = "".join(secrets.choice(alphabet) for _ in range(32))
88+
89+
_oauth_auth_codes[code] = {
90+
"user_id": user_id,
91+
"expires_at": datetime.utcnow() + timedelta(minutes=10)
92+
}
93+
return code
94+
95+
def _get_and_invalidate_auth_code(code: str) -> Optional[str]:
96+
"""Validate auth code and return user_id if valid."""
97+
if code not in _oauth_auth_codes:
98+
return None
99+
100+
data = _oauth_auth_codes[code]
101+
del _oauth_auth_codes[code] # Single-use
102+
103+
if datetime.utcnow() > data["expires_at"]:
104+
return None
105+
106+
return data["user_id"]
107+
108+
79109
# ═══════════════════════════════════════════════════════════════════════════
80110
# Pydantic Models
81111
# ═══════════════════════════════════════════════════════════════════════════
@@ -142,6 +172,16 @@ class MCPExchangeResponse(BaseModel):
142172
user: dict
143173

144174

175+
class OAuthApproveRequest(BaseModel):
176+
"""Request from frontend to approve OAuth and get a code."""
177+
client_id: str
178+
redirect_uri: str
179+
180+
class OAuthApproveResponse(BaseModel):
181+
"""Response with the authorization code."""
182+
code: str
183+
184+
145185
# ═══════════════════════════════════════════════════════════════════════════
146186
# JWT Utilities
147187
# ═══════════════════════════════════════════════════════════════════════════
@@ -475,3 +515,59 @@ async def exchange_mcp_token(request: MCPExchangeRequest):
475515
api_key=key_result["key"],
476516
user=user_response
477517
)
518+
519+
520+
# ═══════════════════════════════════════════════════════════════════════════
521+
# Standard OAuth 2.0 Routes (For ChatGPT UI)
522+
# ═══════════════════════════════════════════════════════════════════════════
523+
524+
@router.post("/oauth/approve", response_model=OAuthApproveResponse)
525+
async def oauth_approve(request: OAuthApproveRequest, current_user: dict = Depends(require_user)):
526+
"""
527+
Called by the Next.js frontend when the user clicks 'Approve' on the consent screen.
528+
Generates an authorization code for standard OAuth 2.0 flow.
529+
"""
530+
if not current_user:
531+
raise HTTPException(status_code=401, detail="Authentication required")
532+
533+
user_id = str(current_user.get("id"))
534+
code = _generate_auth_code(user_id)
535+
return OAuthApproveResponse(code=code)
536+
537+
538+
from fastapi import Form
539+
from fastapi.responses import JSONResponse
540+
541+
@router.post("/oauth/token")
542+
async def oauth_token(
543+
grant_type: str = Form(...),
544+
code: str = Form(None),
545+
redirect_uri: str = Form(None),
546+
client_id: str = Form(None)
547+
):
548+
"""
549+
Standard OAuth 2.0 token endpoint.
550+
ChatGPT calls this directly to exchange the authorization code for an access token.
551+
"""
552+
if grant_type != "authorization_code":
553+
return JSONResponse(status_code=400, content={"error": "unsupported_grant_type"})
554+
555+
if not code:
556+
return JSONResponse(status_code=400, content={"error": "invalid_request", "error_description": "code is required"})
557+
558+
user_id = _get_and_invalidate_auth_code(code)
559+
if not user_id:
560+
return JSONResponse(status_code=400, content={"error": "invalid_grant", "error_description": "Invalid or expired authorization code"})
561+
562+
# Generate a permanent API key acting as the access token
563+
key_result = api_key_store.create_api_key(
564+
user_id=user_id,
565+
name=f"OAuth Client ({client_id or 'Unknown'}) - {datetime.utcnow().strftime('%Y-%m-%d')}"
566+
)
567+
568+
return {
569+
"access_token": key_result["key"],
570+
"token_type": "Bearer",
571+
"expires_in": 31536000, # 1 year
572+
"scope": "all"
573+
}

0 commit comments

Comments
 (0)