Backup automatico script del 2026-08-23 12:04
This commit is contained in:
1 parent
11823447a2
commit
795a15a7b6
66 files changed
+8546
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user