feat: add POST /login handler with PAM and password mode auth

- Import authenticate_pam and create_session_cookie from muxplex.auth
- Import Form, Request from fastapi; RedirectResponse from starlette.responses
- Add @app.post('/login') handler that reads username and password form fields
- In PAM mode: delegates to authenticate_pam(username, password)
- In password mode: compares password to _auth_password
- On success: creates signed muxplex_session cookie (httponly, samesite=strict),
  redirects to / with 303
- On failure: redirects to /login?error=1 with 303
- Add 4 tests covering all branches (password success/failure, PAM success/failure)

Closes task-2-post-login-handler
This commit is contained in:
Brian Krabach
2026-03-28 22:20:38 -07:00
parent d5ffced6a5
commit b5d700214b
2 changed files with 109 additions and 1 deletions
+40 -1
View File
@@ -21,13 +21,16 @@ import websockets
import websockets.exceptions
from websockets.typing import Subprotocol
from fastapi import FastAPI, HTTPException, WebSocket
from fastapi import FastAPI, Form, HTTPException, Request, WebSocket
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from starlette.responses import RedirectResponse
from muxplex.auth import (
AuthMiddleware,
authenticate_pam,
create_session_cookie,
generate_and_save_password,
load_or_create_secret,
load_password,
@@ -485,6 +488,42 @@ async def login_page():
)
@app.post("/login")
async def post_login(
request: Request,
username: str = Form(default=""),
password: str = Form(default=""),
) -> RedirectResponse:
"""Validate credentials and issue a session cookie on success.
In PAM mode, delegates to authenticate_pam(username, password).
In password mode, compares the submitted password to _auth_password.
On success: redirect to / with a signed muxplex_session cookie.
On failure: redirect to /login?error=1.
"""
# Validate credentials
if _auth_mode == "pam":
valid = authenticate_pam(username, password)
else:
valid = password == _auth_password
if not valid:
return RedirectResponse("/login?error=1", status_code=303)
# Issue session cookie
cookie_value = create_session_cookie(_auth_secret, _auth_ttl)
response = RedirectResponse("/", status_code=303)
response.set_cookie(
"muxplex_session",
cookie_value,
httponly=True,
samesite="strict",
max_age=_auth_ttl if _auth_ttl > 0 else None,
)
return response
@app.get("/auth/mode")
async def auth_mode_endpoint():
"""Return the current auth mode and running username."""