Backup automatico script del 2026-08-23 12:04

This commit is contained in:
daniele committed 2026-08-23 12:04:16 +02:00
1 parent 11823447a2
commit 795a15a7b6
66 files changed
+8546

No files matched your search

+322
View File
@@ -0,0 +1,322 @@
# -*- coding: utf-8 -*-
"""OAuth 2.1 Authorization Code + PKCE."""
import base64
import datetime
import hashlib
import json
import os
import secrets
from typing import Optional
from urllib.parse import urlencode, urlparse
from fastapi import HTTPException
from . import auth
from .db import get_conn
from .jwt_utils import (
ACCESS_TOKEN_HOURS,
base_url,
create_access_token,
create_refresh_token,
decode_access_token,
revoke_refresh_token,
scopes_for_user,
consume_refresh_token,
)
def _pkce_valid(code_verifier: str, challenge: str, method: str) -> bool:
if method != "S256":
return False
digest = hashlib.sha256(code_verifier.encode()).digest()
computed = base64.urlsafe_b64encode(digest).decode().rstrip("=")
return computed == challenge
TRUSTED_REDIRECT_URIS = frozenset({
"https://claude.ai/api/mcp/auth_callback",
"https://chatgpt.com/connector_platform_oauth_redirect",
"https://chat.openai.com/connector_platform_oauth_redirect",
# Cursor IDE / Agents (docs.cursor.com/mcp)
"https://www.cursor.com/agents/mcp/oauth/callback",
"http://localhost:8787/callback",
# Legacy Cursor desktop
"cursor://anysphere.cursor-mcp/oauth/callback",
})
def ensure_default_client() -> None:
clients = [
(
os.environ.get("MCP_OAUTH_CLIENT_ID", "loogle-mcp-public"),
"Loogle MCP Public",
[
"https://chatgpt.com/connector_platform_oauth_redirect",
"https://chat.openai.com/connector_platform_oauth_redirect",
"https://claude.ai/api/mcp/auth_callback",
"https://www.cursor.com/agents/mcp/oauth/callback",
"http://localhost:8787/callback",
"cursor://anysphere.cursor-mcp/oauth/callback",
"http://127.0.0.1:*/callback",
"http://localhost:*/callback",
],
),
(
"cursor",
"Cursor IDE",
[
"https://www.cursor.com/agents/mcp/oauth/callback",
"http://localhost:8787/callback",
"cursor://anysphere.cursor-mcp/oauth/callback",
],
),
(
"claude-desktop",
"Claude Desktop/App",
["https://claude.ai/api/mcp/auth_callback"],
),
(
"claude-ai",
"Claude.ai",
["https://claude.ai/api/mcp/auth_callback"],
),
]
conn = get_conn()
for client_id, client_name, redirect_uris in clients:
row = conn.execute(
"SELECT client_id FROM oauth_clients WHERE client_id=?", (client_id,)
).fetchone()
if row:
conn.execute(
"UPDATE oauth_clients SET client_name=?, redirect_uris=? WHERE client_id=?",
(client_name, json.dumps(redirect_uris), client_id),
)
else:
conn.execute(
"INSERT INTO oauth_clients(client_id,client_name,redirect_uris) VALUES (?,?,?)",
(client_id, client_name, json.dumps(redirect_uris)),
)
conn.commit()
def _is_trusted_redirect_uri(redirect_uri: str) -> bool:
if redirect_uri in TRUSTED_REDIRECT_URIS:
return True
parsed = urlparse(redirect_uri)
if parsed.scheme == "http" and parsed.hostname in ("127.0.0.1", "localhost"):
if (parsed.path or "").endswith("/callback"):
return True
return False
def register_client(client_name: str, redirect_uris: list[str]) -> dict:
client_id = secrets.token_urlsafe(16)
for uri in redirect_uris:
if not _is_trusted_redirect_uri(uri):
raise HTTPException(
status_code=400,
detail=f"Redirect URI non consentito: {uri}",
)
get_conn().execute(
"INSERT INTO oauth_clients(client_id,client_name,redirect_uris) VALUES (?,?,?)",
(client_id, client_name, json.dumps(redirect_uris)),
)
get_conn().commit()
return {"client_id": client_id, "client_name": client_name, "redirect_uris": redirect_uris}
def _client_redirect_uris(client_id: str) -> list[str]:
row = get_conn().execute(
"SELECT redirect_uris FROM oauth_clients WHERE client_id=?", (client_id,)
).fetchone()
if not row:
return []
return json.loads(row["redirect_uris"])
def _ensure_client_for_redirect(client_id: str, redirect_uri: str) -> None:
"""Registra client OAuth al volo (Claude usa spesso client_id = username)."""
conn = get_conn()
row = conn.execute(
"SELECT redirect_uris FROM oauth_clients WHERE client_id=?", (client_id,)
).fetchone()
if row:
uris = set(json.loads(row["redirect_uris"]))
if redirect_uri not in uris:
uris.add(redirect_uri)
conn.execute(
"UPDATE oauth_clients SET redirect_uris=? WHERE client_id=?",
(json.dumps(sorted(uris)), client_id),
)
conn.commit()
return
conn.execute(
"INSERT INTO oauth_clients(client_id,client_name,redirect_uris) VALUES (?,?,?)",
(client_id, f"MCP client {client_id}", json.dumps([redirect_uri])),
)
conn.commit()
def _redirect_allowed(client_id: str, redirect_uri: str) -> bool:
if _is_trusted_redirect_uri(redirect_uri):
_ensure_client_for_redirect(client_id, redirect_uri)
return True
allowed = _client_redirect_uris(client_id)
if redirect_uri in allowed:
return True
parsed = urlparse(redirect_uri)
for pattern in allowed:
if "*" in pattern:
pp = urlparse(pattern.replace("*", "placeholder"))
if parsed.scheme == pp.scheme and parsed.netloc.endswith(pp.netloc.split("placeholder")[-1]):
return True
return False
def create_auth_code(
client_id: str,
user_id: int,
redirect_uri: str,
scope: str,
code_challenge: Optional[str],
code_challenge_method: Optional[str],
) -> str:
code = secrets.token_urlsafe(32)
expires = (
datetime.datetime.utcnow() + datetime.timedelta(minutes=10)
).strftime("%Y-%m-%d %H:%M:%S")
get_conn().execute(
"INSERT INTO oauth_codes(code,client_id,user_id,redirect_uri,scope,code_challenge,code_challenge_method,expires_at)"
" VALUES (?,?,?,?,?,?,?,?)",
(code, client_id, user_id, redirect_uri, scope, code_challenge, code_challenge_method, expires),
)
get_conn().commit()
return code
def exchange_code(
code: str,
client_id: str,
redirect_uri: str,
code_verifier: Optional[str],
) -> dict:
row = get_conn().execute(
"SELECT * FROM oauth_codes WHERE code=? AND used=0 AND expires_at > datetime('now')",
(code,),
).fetchone()
if not row:
raise HTTPException(400, "Codice non valido o scaduto")
row = dict(row)
if row["client_id"] != client_id or row["redirect_uri"] != redirect_uri:
raise HTTPException(400, "Client o redirect URI non validi")
if row.get("code_challenge"):
if not code_verifier or not _pkce_valid(code_verifier, row["code_challenge"], row.get("code_challenge_method") or "S256"):
raise HTTPException(400, "PKCE verification failed")
user = auth.get_user_by_id(row["user_id"])
if not user:
raise HTTPException(400, "Utente non trovato")
get_conn().execute("UPDATE oauth_codes SET used=1 WHERE code=?", (code,))
get_conn().commit()
scope = scopes_for_user(user, row["scope"])
access_token, _ = create_access_token(user, scope, client_id)
refresh = create_refresh_token(user["id"], scope, client_id)
return {
"access_token": access_token,
"token_type": "Bearer",
"expires_in": ACCESS_TOKEN_HOURS * 3600,
"refresh_token": refresh,
"scope": scope,
}
def refresh_access_token(refresh_token: str, client_id: str) -> dict:
row = consume_refresh_token(refresh_token)
if not row or row["client_id"] != client_id:
raise HTTPException(400, "Refresh token non valido")
user = auth.get_user_by_id(row["user_id"])
if not user:
raise HTTPException(400, "Utente non trovato")
revoke_refresh_token(refresh_token)
scope = row["scope"]
access_token, _ = create_access_token(user, scope, client_id)
refresh = create_refresh_token(user["id"], scope, client_id)
return {
"access_token": access_token,
"token_type": "Bearer",
"expires_in": ACCESS_TOKEN_HOURS * 3600,
"refresh_token": refresh,
"scope": scope,
}
def authorization_server_metadata() -> dict:
base = base_url()
return {
"issuer": base,
"authorization_endpoint": f"{base}/authorize",
"token_endpoint": f"{base}/token",
"registration_endpoint": f"{base}/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
"scopes_supported": [
"context:read",
"context:write",
"knowledge:read",
"knowledge:write",
"gitea:read",
"gitea:write",
"home:read",
"irrigation:read",
"turni:read",
"admin",
],
}
def protected_resource_metadata() -> dict:
base = base_url()
return {
"resource": f"{base}/mcp",
"authorization_servers": [base],
"scopes_supported": [
"context:read",
"context:write",
"knowledge:read",
"knowledge:write",
"gitea:read",
"gitea:write",
"home:read",
"irrigation:read",
"turni:read",
],
"bearer_methods_supported": ["header"],
}
def build_authorize_redirect(
client_id: str,
redirect_uri: str,
scope: str,
state: str,
code_challenge: Optional[str],
code_challenge_method: Optional[str],
user_id: int,
) -> str:
if not _redirect_allowed(client_id, redirect_uri):
raise HTTPException(400, "Redirect URI non autorizzato")
code = create_auth_code(
client_id, user_id, redirect_uri, scope, code_challenge, code_challenge_method
)
params = {"code": code, "state": state}
sep = "&" if "?" in redirect_uri else "?"
return f"{redirect_uri}{sep}{urlencode(params)}"
def bearer_claims_from_header(authorization: str) -> Optional[dict]:
if not authorization.lower().startswith("bearer "):
return None
token = authorization[7:].strip()
return decode_access_token(token)