From f5153b711c097e3913c48b527110b14843b18906 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 23 Feb 2026 23:26:10 +0000 Subject: [PATCH] Add artdag to OAuth clients + POST /auth/oauth/token endpoint Standard HTTP token exchange for clients that don't share the coop DB. Returns user_id, username, display_name, grant_token in exchange for a valid authorization code. Co-Authored-By: Claude Opus 4.6 --- bp/auth/routes.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/bp/auth/routes.py b/bp/auth/routes.py index ce04927..7e40820 100644 --- a/bp/auth/routes.py +++ b/bp/auth/routes.py @@ -43,7 +43,7 @@ from .services import ( SESSION_USER_KEY = "uid" ACCOUNT_SESSION_KEY = "account_sid" -ALLOWED_CLIENTS = {"blog", "market", "cart", "events", "federation"} +ALLOWED_CLIENTS = {"blog", "market", "cart", "events", "federation", "artdag"} def register(url_prefix="/auth"): @@ -121,6 +121,69 @@ def register(url_prefix="/auth"): f"&account_did={account_did}" ) + # --- OAuth2 token exchange (for external clients like artdag) ------------- + + @auth_bp.post("/oauth/token") + @auth_bp.post("/oauth/token/") + async def oauth_token(): + """Exchange an authorization code for user info + grant token. + + Used by clients that don't share the coop database (e.g. artdag). + Accepts JSON: {code, client_id, redirect_uri} + Returns JSON: {user_id, username, display_name, grant_token} + """ + data = await request.get_json() + if not data: + return jsonify({"error": "invalid_request"}), 400 + + code = data.get("code", "") + client_id = data.get("client_id", "") + redirect_uri = data.get("redirect_uri", "") + + if client_id not in ALLOWED_CLIENTS: + return jsonify({"error": "invalid_client"}), 400 + + now = datetime.now(timezone.utc) + + async with get_session() as s: + async with s.begin(): + result = await s.execute( + select(OAuthCode) + .where(OAuthCode.code == code) + .with_for_update() + ) + oauth_code = result.scalar_one_or_none() + + if not oauth_code: + return jsonify({"error": "invalid_grant"}), 400 + + if oauth_code.used_at is not None: + return jsonify({"error": "invalid_grant"}), 400 + + if oauth_code.expires_at < now: + return jsonify({"error": "invalid_grant"}), 400 + + if oauth_code.client_id != client_id: + return jsonify({"error": "invalid_grant"}), 400 + + if oauth_code.redirect_uri != redirect_uri: + return jsonify({"error": "invalid_grant"}), 400 + + oauth_code.used_at = now + user_id = oauth_code.user_id + grant_token = oauth_code.grant_token + + user = await s.get(User, user_id) + if not user: + return jsonify({"error": "invalid_grant"}), 400 + + return jsonify({ + "user_id": user_id, + "username": user.email or "", + "display_name": user.name or "", + "grant_token": grant_token, + }) + # --- Grant verification (internal endpoint) ------------------------------ @auth_bp.get("/internal/verify-grant")