This document describes the security model of the AI Customer Support Agent, including current implementations, known demo shortcuts, and recommendations for production hardening.
The system uses stateless JWT authentication via the python-jose library.
How it works:
- User submits credentials to
POST /auth/login - Backend verifies email/password against the
userstable - On success, a JWT is generated with payload:
{ "sub": "USER-001", "role": "customer", "exp": 1705312200 } - Token is signed with
HS256algorithm and the configuredJWT_SECRET - Token expires after 24 hours
- Client includes the token in the
Authorization: Bearer <token>header on all protected requests
Implementation file: mini-services/refund-backend/routes/auth.py
The get_current_user FastAPI dependency:
- Extracts the
Authorizationheader from the incomingRequest - Validates the
Bearerprefix - Decodes the JWT using the configured secret and algorithm
- Checks expiration (handled by
python-jose) - Looks up the user in the database (ensures user still exists)
- Returns the
UserORM object for use in route handlers
Two roles are defined: admin and customer.
| Endpoint | Admin | Customer | Public |
|---|---|---|---|
GET /health |
✅ | ✅ | ✅ |
POST /auth/login |
✅ | ✅ | ✅ |
GET /auth/me |
✅ | ✅ | — |
POST /chat |
✅ | ✅ | — |
GET /refunds |
✅ (all) | ✅ (own) | — |
GET /refunds/{id} |
✅ | ✅ (own) | — |
POST /refunds |
✅ | ✅ (own order) | — |
GET /customers |
✅ | ❌ | — |
GET /customers/{id} |
✅ | ❌ | — |
GET /orders |
✅ (all) | ✅ (own) | — |
GET /orders/{id} |
✅ | ✅ (own) | — |
GET /policies |
✅ | ❌ | — |
PUT /policies/{id} |
✅ | ❌ | — |
GET /logs |
✅ | ❌ | — |
GET /logs/agent-executions |
✅ | ❌ | — |
GET /logs/agent-executions/{id} |
✅ | ❌ | — |
get_current_user: FastAPI dependency that extracts and validates the JWT, returnsUserobjectrequire_admin: FastAPI dependency that wrapsget_current_userand checksuser.role == "admin", returning 403 if not- Route-level filtering: Customer routes filter queries by
current_user.customer_idto enforce data isolation
# Example: Customer can only see their own refunds
if current_user.role == "customer":
q = q.filter(RefundRequest.customer_id == current_user.customer_id)All API request bodies are validated by Pydantic models in schemas.py:
LoginRequest: Requiresemail(string) andpassword(string)ChatRequest: Requiresmessage(string) anduser_id(string); optionalconversation_idRefundRequestCreate: Requiresorder_id,customer_id,reason; optionalamountRefundPolicyUpdate: All fields optional (value,is_active,description)
Pydantic automatically rejects:
- Missing required fields (400 Bad Request)
- Wrong data types (422 Unprocessable Entity)
- Extra fields not in the schema
FastAPI's Query type is used for bounded parameters:
@router.get("", response_model=list[AuditLogOut])
def list_logs(
limit: int = Query(100, ge=1, le=1000), # Must be 1-1000
offset: int = Query(0, ge=0), # Must be non-negative
...Path parameters (e.g., {refund_id}, {customer_id}) are typed as strings. The backend queries the database and returns 404 if not found — there is no risk of path traversal since SQLAlchemy uses parameterized queries.
Status: ✅ Fully protected
The entire application uses SQLAlchemy ORM with parameterized queries. There is no raw SQL anywhere in the codebase.
# Safe — SQLAlchemy parameterizes this
user = db.query(User).filter(User.email == body.email).first()
# The generated SQL is:
# SELECT * FROM users WHERE email = ? -- with body.email as a bound parameterThe filter() method never interpolates user input into the SQL string. Even if a user submitted a malicious email like ' OR 1=1 --, SQLAlchemy would treat it as a literal string value.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # All origins allowed
allow_credentials=True,
allow_methods=["*"], # All HTTP methods
allow_headers=["*"], # All headers
)Restrict allow_origins to the actual frontend domain(s):
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://shopmart.example.com",
"https://admin.shopmart.example.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT"],
allow_headers=["Authorization", "Content-Type"],
)Alternatively, use an environment variable:
import os
ALLOWED_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT"],
allow_headers=["Authorization", "Content-Type"],
)Passwords are stored and compared as plaintext in the users table.
# In routes/auth.py — DEMO ONLY
if not user or user.password != body.password:
raise HTTPException(status_code=401)This is intentional for demo convenience. All demo passwords are simple (admin123, password1, etc.) so presenters can type them quickly.
Use bcrypt for password hashing:
pip install bcryptfrom passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# On user creation:
hashed_password = pwd_context.hash(plain_password)
# On login verification:
if not pwd_context.verify(body.password, user.password):
raise HTTPException(status_code=401)Update the seed data to use hashed passwords:
# In seed.py
hashed = pwd_context.hash("admin123")
db.add(User(id="USER-ADMIN", email="admin@shopmart.com", password=hashed, ...))- Secret: Hardcoded as
"demo-secret-key-change-in-production"inconfig.py - Algorithm: HS256
- Expiry: 24 hours
- No refresh tokens
-
Use a strong, random secret (minimum 32 characters):
import secrets # Generate: secrets.token_urlsafe(32) JWT_SECRET = os.getenv("JWT_SECRET") # Must be set
-
Add refresh tokens for longer sessions:
- Issue a short-lived access token (15 min) and a long-lived refresh token (7 days)
- Implement
POST /auth/refreshto exchange a refresh token for a new access token - Store refresh tokens in the database for revocation capability
-
Consider rotating secrets periodically
-
Validate the
algheader to prevent algorithm confusion attacks (python-jose does this by default)
There is no rate limiting on any endpoint.
Install slowapi:
pip install slowapifrom slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
# Apply to login endpoint
@app.post("/auth/login")
@limiter.limit("5/minute") # 5 login attempts per minute per IP
def login(request: Request, body: LoginRequest, ...):
...
# Apply to chat endpoint
@app.post("/chat")
@limiter.limit("10/minute") # 10 messages per minute per user
def chat(request: Request, body: ChatRequest, ...):
...Key endpoints to rate-limit:
POST /auth/login— Prevent brute-force attacks (5-10/minute)POST /chat— Prevent abuse and LLM resource exhaustion (10-20/minute)
- Prompt injection: A user could attempt to manipulate the system prompt
- Tool misuse: The LLM might call tools with unexpected parameters
- System prompt is server-side — Users cannot modify it
- Tool parameters are validated by Pydantic schemas and the tool functions themselves
- Tools return structured data — The agent makes decisions based on tool outputs, not user claims
- No destructive tools — Tools only read data and create records; nothing is deleted or modified except new inserts
- Policy checks are code-enforced — Even if the LLM approves something, the tool layer enforces constraints (e.g.,
check_refund_windowqueries the database, doesn't trust the LLM)
- Sanitize user input before including in LLM context (strip excessively long messages)
- Add a maximum message length to prevent token-bombing
- Log all LLM inputs/outputs for security auditing (already done via
AgentExecution) - Consider output filtering to prevent the LLM from leaking system information
| Item | Current State | Recommendation |
|---|---|---|
| Password storage | Plaintext | Implement bcrypt hashing |
| JWT secret | Hardcoded | Use env var with 32+ char random value |
| CORS | allow_origins=["*"] |
Restrict to specific domains |
| Rate limiting | None | Add slowapi to login and chat |
| HTTPS | Not configured | Add TLS termination (Caddy/nginx) |
| Item | Current State | Recommendation |
|---|---|---|
| Input size limits | None | Max message length (e.g., 2000 chars) |
| Token blacklist | None | Store revoked tokens for logout support |
| Logging | Python logging | Structured JSON logging for SIEM integration |
| Dependency scanning | None | Run pip audit and npm audit regularly |
| Item | Current State | Recommendation |
|---|---|---|
| CSP headers | Not set | Add Content-Security-Policy headers |
| HSTS | Not set | Enable HTTP Strict Transport Security |
| API versioning | None | Add /api/v1/ prefix for future compatibility |
| Request ID tracking | None | Add X-Request-ID header for distributed tracing |