"""Auth routes: auth-hub SSO login / logout / refresh. Local email/password login and registration have been removed: every account now comes from auth-hub, the centralized SSO provider. Web login and Garmin account authorization are deliberately separate flows — see services/garmin_auth.py and routes/garmin.py for the latter. """ from flask import Blueprint, request, g, jsonify from auth import sign_token, require_auth from db import execute from services.auth_hub_client import ( get_authorization_url, exchange_code_for_token, get_userinfo, find_or_create_user, ) bp = Blueprint("auth", __name__) @bp.route("/logout", methods=["POST"]) @require_auth def logout(): execute("UPDATE users SET jwt_token = NULL WHERE id = ?", [g.user_id]) return jsonify({"message": "ok"}) @bp.route("/refresh", methods=["POST"]) @require_auth def refresh(): token = sign_token(g.user_id) execute("UPDATE users SET jwt_token = ? WHERE id = ?", [token, g.user_id]) return jsonify({"token": token}) # --- OAuth2 with auth-hub (unified SSO) --- @bp.route("/callback", methods=["GET"]) def auth_hub_callback(): """Handle OAuth callback from auth-hub.""" code = request.args.get("code") state = request.args.get("state") error = request.args.get("error") if error: return jsonify({"error": f"auth-hub error: {error}"}), 400 if not code: return jsonify({"error": "missing authorization code"}), 400 # TODO: Verify state parameter matches what we stored # For MVP, we'll skip this check # Get code_verifier from somewhere (store in session or request context) # This is a limitation of GET-only callback; in production use session storage # For now, request it from the frontend via a separate endpoint code_verifier = request.args.get("code_verifier") if not code_verifier: return jsonify({"error": "missing code_verifier"}), 400 try: # Exchange code for tokens token_response = exchange_code_for_token(code, code_verifier) access_token = token_response.get("access_token") # Get user info from auth-hub userinfo = get_userinfo(access_token) auth_hub_sub = userinfo.get("sub") auth_hub_username = userinfo.get("preferred_username") if not auth_hub_sub or not auth_hub_username: return jsonify({"error": "invalid userinfo response"}), 400 # Find or create user in our database user_id = find_or_create_user(auth_hub_sub, auth_hub_username) # Generate our own JWT token token = sign_token(user_id) execute("UPDATE users SET jwt_token = ? WHERE id = ?", [token, user_id]) # Return token to frontend (frontend will store in localStorage/cookie) return jsonify({ "ok": True, "id": user_id, "token": token, "username": auth_hub_username, }) except Exception as e: return jsonify({"error": f"token exchange failed: {str(e)}"}), 400 @bp.route("/auth-hub/start", methods=["POST"]) def auth_hub_start(): """Initiate auth-hub login flow, return URL and PKCE verifier.""" auth_url, code_verifier, state = get_authorization_url() # Frontend will store code_verifier and state in sessionStorage # and return it in the callback return jsonify({ "auth_url": auth_url, "code_verifier": code_verifier, "state": state, })