This commit is contained in:
Waylon Walker 2025-11-21 13:47:16 -06:00
parent bd77731487
commit 77d0c05a64
3 changed files with 83 additions and 18 deletions

View file

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