From 77d0c05a64ffe38a768a1fb1b892244f7451efd6 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 21 Nov 2025 13:47:16 -0600 Subject: [PATCH] wip --- justfile | 2 +- main_auth.py | 75 +++++++++++++++++++++++++++++++++++++++++++++------- nginx.conf | 24 +++++++++++------ 3 files changed, 83 insertions(+), 18 deletions(-) diff --git a/justfile b/justfile index 7a423f4..6c9100c 100644 --- a/justfile +++ b/justfile @@ -2,7 +2,7 @@ default: @just --choose start-auth: - ./main_auth.py & + ./main_auth.py stop-auth: pkill -f main_auth.py || true diff --git a/main_auth.py b/main_auth.py index 419223d..b050f92 100755 --- a/main_auth.py +++ b/main_auth.py @@ -6,6 +6,7 @@ # "uvicorn[standard]", # "python-jose[cryptography]", # "python-multipart", +# "rich", # ] # /// from fastapi import FastAPI, Request, Response, HTTPException, Depends @@ -14,12 +15,32 @@ from fastapi.staticfiles import StaticFiles from fastapi.security import HTTPBasic, HTTPBasicCredentials import secrets from jose import JWTError, jwt -from datetime import datetime, timedelta +import datetime import os +import logging +from rich.logging import RichHandler +from rich.console import Console + +# Configure rich console for consistent styling +console = Console() app = FastAPI() security = HTTPBasic() +# Configure structured logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(name)-12s | %(levelname)-8s | %(message)s", + datefmt="%H:%M:%S", + handlers=[RichHandler(console=console, show_path=False, rich_tracebacks=True)] +) + +logger = logging.getLogger("nginx-auth") + +# Configure uvicorn loggers to use our format +uvicorn_error_logger = logging.getLogger("uvicorn.error") +uvicorn_error_logger.handlers = [RichHandler(console=console, show_path=False)] + # JWT Configuration SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-super-secure-secret-key-change-this-in-production") ALGORITHM = "HS256" @@ -30,12 +51,12 @@ USERS = { "reader": {"password": "reader", "role": "reader"}, } -def create_access_token(data: dict, expires_delta: timedelta = None): +def create_access_token(data: dict, expires_delta: datetime.timedelta = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.datetime.now(datetime.UTC) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.datetime.now(datetime.UTC) + datetime.timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -64,17 +85,21 @@ def get_current_role(user: str): return USERS[user]["role"] @app.post("/login") -async def login(credentials: HTTPBasicCredentials = Depends(security)): +async def login(request: Request, credentials: HTTPBasicCredentials = Depends(security)): user = credentials.username pwd = credentials.password + client_ip = request.client.host if request.client else "unknown" + if user in USERS and secrets.compare_digest(USERS[user]['password'], pwd): # Create JWT token - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token_expires = datetime.timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user, "role": USERS[user]["role"]}, expires_delta=access_token_expires ) + logger.info(f"🔑 LOGIN SUCCESS | {user}({USERS[user]['role']}) | {client_ip}") + resp = Response("OK", status_code=200) resp.set_cookie( "access_token", @@ -89,10 +114,22 @@ async def login(credentials: HTTPBasicCredentials = Depends(security)): resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" resp.headers["Pragma"] = "no-cache" return resp + + logger.info(f"🔑 LOGIN FAILED | {user} | {client_ip}") raise HTTPException(status_code=401, detail="Invalid credentials") @app.get("/logout") -def logout(): +def logout(request: Request): + client_ip = request.client.host if request.client else "unknown" + + # Try to get current user for logging + token = request.cookies.get("access_token") + username = "unknown" + if token: + username = verify_jwt_token(token) or "unknown" + + logger.info(f"👋 LOGOUT | {username} | {client_ip}") + resp = RedirectResponse("/") resp.delete_cookie("access_token", path="/") # Ensure logout response isn't cached @@ -104,20 +141,29 @@ def logout(): @app.get("/authz") def authz(request: Request): token = request.cookies.get("access_token") - path = request.headers.get("X-Original-URI") + path = request.headers.get("X-Original-URI", "unknown") + method = request.headers.get("X-Original-Method", "GET") + host = request.headers.get("X-Original-Host", "localhost") + client_ip = request.client.host if request.client else "unknown" if not token: + logger.info(f"🚫 AUTH DENIED | {method:4} {path} | No token | {client_ip}") return Response("Not authenticated", status_code=401) username = verify_jwt_token(token) if not username or username not in USERS: + logger.info(f"🚫 AUTH DENIED | {method:4} {path} | Invalid token | {client_ip}") return Response("Invalid token", status_code=401) user_role = USERS[username]['role'] + # Only admin may access /admin if path and path.startswith("/admin") and user_role != 'admin': + logger.info(f"🚫 AUTH DENIED | {method:4} {path} | {username}({user_role}) | {client_ip}") return Response("Forbidden", status_code=403) + # Everything else: allowed + logger.info(f"✅ AUTH ALLOW | {method:4} {path} | {username}({user_role}) | {client_ip}") return Response("OK", status_code=200) @app.get("/me") @@ -139,5 +185,16 @@ def get_current_user_info(request: Request): if __name__ == "__main__": import uvicorn + app.mount("/static", StaticFiles(directory="static"), name="static") - uvicorn.run(app, host="localhost", port=5115) + + logger.info("🚀 Starting nginx-auth demo server...") + + # Disable uvicorn access logs since we have our own structured auth logging + uvicorn.run( + app, + host="localhost", + port=5115, + access_log=False, # Disable to avoid conflicts + log_level="info" + ) diff --git a/nginx.conf b/nginx.conf index 76e69ba..deeb40e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,14 +12,14 @@ http { root /usr/share/nginx/html; index index.html; - # Custom error pages + # Custom error pages (preserve original status codes) error_page 403 /403/; error_page 404 /404/; location / { auth_request /authz; error_page 401 = @login; # If not authed, redirect to login page - error_page 403 = @forbidden; # If forbidden, show custom 403 page + error_page 403 @forbidden; # If forbidden, show custom 403 page # Disable all caching for demo purposes add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; @@ -32,6 +32,9 @@ http { internal; proxy_pass http://127.0.0.1:5115/authz; proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Original-Host $host; + proxy_pass_request_body off; proxy_set_header Content-Length ""; } @@ -39,6 +42,15 @@ http { add_header Content-Type text/html; return 302 http://localhost:8000/login/; } + + location @forbidden { + internal; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Pragma "no-cache"; + add_header Expires "Thu, 01 Jan 1970 00:00:00 GMT"; + # Serve the custom 403 page without changing status code + try_files /403/index.html =403; + } location /me { auth_request /authz; error_page 401 = @login; # If not authed, redirect to login page @@ -52,12 +64,7 @@ http { proxy_pass http://localhost:5115/me; } - location @forbidden { - add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; - add_header Pragma "no-cache"; - add_header Expires "Thu, 01 Jan 1970 00:00:00 GMT"; - rewrite ^.*$ /403/ last; - } + @@ -80,6 +87,7 @@ http { # Custom error pages are public and shouldn't be cached location ~ ^/(403|404)/$ { + internal; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; add_header Pragma "no-cache"; add_header Expires "Thu, 01 Jan 1970 00:00:00 GMT";