feat: implement HMAC-based JWT authentication and secure API endpoints with dependency injection

This commit is contained in:
2026-06-16 12:18:44 +05:30
parent 9baf93bfef
commit 9987b3a45a

View File

@@ -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.
"""