From 30351f1bcf7b352bb0afd6d5bf2fa0029bbd08e5 Mon Sep 17 00:00:00 2001 From: Stefan Lohmaier Date: Fri, 12 Jun 2026 09:13:54 +0200 Subject: [PATCH] Switch to per-service subdomains, shared token store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit URLs: mail.mcp.home.slohmaier.de, calendar.mcp..., etc. No more path-prefix routing — each service has its own domain. OAuth tokens stored in shared .active_tokens.json file so all services can validate tokens issued by any service. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + common.py | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a1b7fba..5710f17 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ *.pyc tokens.json config.json +.active_tokens.json diff --git a/common.py b/common.py index 20ee7fa..f7b0f79 100644 --- a/common.py +++ b/common.py @@ -36,10 +36,23 @@ def load_config(): _tokens_cache = None _current_user: contextvars.ContextVar[str | None] = contextvars.ContextVar("current_user", default=None) -_access_tokens: dict[str, dict] = {} +_TOKEN_STORE = os.path.join(BASE_DIR, ".active_tokens.json") _auth_codes: dict[str, dict] = {} +def _load_access_tokens(): + try: + with open(_TOKEN_STORE) as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return {} + + +def _save_access_tokens(tokens): + with open(_TOKEN_STORE, "w") as f: + json.dump(tokens, f) + + def _load_tokens(): global _tokens_cache if _tokens_cache is None: @@ -65,11 +78,13 @@ def _resolve_client(client_id, client_secret): def _resolve_access_token(token): - info = _access_tokens.get(token) + tokens = _load_access_tokens() + info = tokens.get(token) if not info: return None if info.get("expires_at", 0) < time.time(): - del _access_tokens[token] + del tokens[token] + _save_access_tokens(tokens) return None return info["user"] @@ -87,8 +102,7 @@ def _verify_pkce(code_verifier, code_challenge, method): async def oauth_metadata(request: Request): proto = request.headers.get("x-forwarded-proto", request.url.scheme) host = request.headers.get("host", "") - prefix = request.headers.get("x-forwarded-prefix", "") - base = f"{proto}://{host}{prefix}" + base = f"{proto}://{host}" return JSONResponse({ "issuer": base, "authorization_endpoint": base + "/authorize", @@ -171,7 +185,12 @@ async def oauth_token(request: Request): access_token = secrets.token_urlsafe(48) expires_in = 86400 - _access_tokens[access_token] = {"user": user, "expires_at": time.time() + expires_in} + tokens = _load_access_tokens() + # Cleanup expired + now = time.time() + tokens = {k: v for k, v in tokens.items() if v.get("expires_at", 0) > now} + tokens[access_token] = {"user": user, "expires_at": now + expires_in} + _save_access_tokens(tokens) return JSONResponse({ "access_token": access_token,