From 9987b3a45ac2958a35097e71a259b918b5063ae1 Mon Sep 17 00:00:00 2001 From: kaushik Date: Tue, 16 Jun 2026 12:18:44 +0530 Subject: [PATCH] feat: implement HMAC-based JWT authentication and secure API endpoints with dependency injection --- central_api_prototype.py | 82 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/central_api_prototype.py b/central_api_prototype.py index 135ba63..5cb1f50 100644 --- a/central_api_prototype.py +++ b/central_api_prototype.py @@ -11,12 +11,17 @@ else: # Fallback to load default .env if any variables are not yet defined load_dotenv() -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Depends, Header from fastapi.responses import PlainTextResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import Dict, Any, List import uvicorn +import base64 +import hmac +import hashlib +import json +from datetime import datetime, timedelta app = FastAPI(title="RMM Central API") @@ -29,6 +34,58 @@ app.add_middleware( allow_headers=["*"], ) +# JWT-like HMAC Secure Token Utilities +JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "seekright_rmm_secret_key_2026_default") + +def generate_token(username: str) -> str: + # Expire in 1 day (24 hours) + expiry = (datetime.utcnow() + timedelta(days=1)).isoformat() + payload = { + "username": username, + "expires": expiry + } + payload_json = json.dumps(payload) + payload_b64 = base64.urlsafe_b64encode(payload_json.encode('utf-8')).decode('utf-8').rstrip('=') + + sig = hmac.new(JWT_SECRET_KEY.encode('utf-8'), payload_b64.encode('utf-8'), hashlib.sha256).hexdigest() + return f"{payload_b64}.{sig}" + +def verify_token(token: str) -> bool: + try: + parts = token.split(".") + if len(parts) != 2: + return False + payload_b64, sig = parts + + # Verify signature + expected_sig = hmac.new(JWT_SECRET_KEY.encode('utf-8'), payload_b64.encode('utf-8'), hashlib.sha256).hexdigest() + if not hmac.compare_digest(sig, expected_sig): + return False + + # Decode and check expiry + padding = 4 - (len(payload_b64) % 4) + if padding < 4: + payload_b64 += "=" * padding + payload_json = base64.urlsafe_b64decode(payload_b64.encode('utf-8')).decode('utf-8') + payload = json.loads(payload_json) + + expires_dt = datetime.fromisoformat(payload["expires"]) + if datetime.utcnow() > expires_dt: + return False # Expired + + return payload["username"] == "root" + except Exception: + return False + +# FastAPI dependency to secure UI endpoints +async def get_current_user(authorization: str = Header(None)): + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Missing or invalid authentication credentials") + token = authorization.split(" ")[1] + if not verify_token(token): + raise HTTPException(status_code=401, detail="Authentication token is invalid or has expired") + return "root" + import os import json from datetime import datetime @@ -239,6 +296,17 @@ class CommandResultPayload(BaseModel): stdout: str stderr: str +class LoginPayload(BaseModel): + username: str + password: str + +@app.post("/api/login") +async def login(payload: LoginPayload): + if payload.username == "root" and payload.password == "seekright159@": + token = generate_token("root") + return {"token": token} + raise HTTPException(status_code=401, detail="Invalid username or password") + @app.get("/api/get-commands") async def get_whitelisted_commands(platform: str): """ @@ -284,7 +352,7 @@ async def get_command(client_id: str): return {"command": cmd} @app.post("/api/schedule-command") -async def schedule_command(client_id: str, command: str): +async def schedule_command(client_id: str, command: str, current_user: str = Depends(get_current_user)): """ Endpoint for your Dashboard/UI to schedule a new command. Supports a comma-separated list of client IDs for batch fleet updates. @@ -401,7 +469,7 @@ async def receive_telemetry(client_id: str, payload: TelemetryPayload): return {"status": "success"} @app.get("/api/telemetry") -async def get_all_telemetry(): +async def get_all_telemetry(current_user: str = Depends(get_current_user)): """ Endpoint to view the live dashboard data of all clients. """ @@ -409,14 +477,14 @@ async def get_all_telemetry(): return {cid: info.get("telemetry", {}) for cid, info in clients.items() if info.get("telemetry")} @app.get("/api/clients") -async def get_clients_api(): +async def get_clients_api(current_user: str = Depends(get_current_user)): """ Endpoint for the React UI to fetch the live active client registry with computed statuses. """ return load_clients() @app.get("/api/logs") -async def get_logs_history(): +async def get_logs_history(current_user: str = Depends(get_current_user)): """ Endpoint to retrieve logs history grouped by client_id. """ @@ -453,7 +521,7 @@ async def receive_command_result(client_id: str, payload: CommandResultPayload): return {"status": "success"} @app.get("/api/get-raw-commands") -async def get_raw_commands(): +async def get_raw_commands(current_user: str = Depends(get_current_user)): """ Endpoint for the UI to load the complete commands.json whitelist file. """ @@ -465,7 +533,7 @@ async def get_raw_commands(): raise HTTPException(status_code=500, detail=f"Failed to read commands: {e}") @app.post("/api/save-commands") -async def save_commands(commands: Dict[str, Any]): +async def save_commands(commands: Dict[str, Any], current_user: str = Depends(get_current_user)): """ Endpoint for the UI to save changes back to commands.json. """