Files
orion/docs/api/authentication.md
Samir Boulahtit cbfbbb4654 fix: customer authentication and shop error page styling
## Customer Authentication Fixes
- Fix get_current_customer_api to properly decode customer tokens (was using User model)
- Add _validate_customer_token() helper for shared customer token validation
- Add vendor validation: token.vendor_id must match request URL vendor
- Block admin/vendor tokens from shop endpoints (type != "customer")
- Update get_current_customer_optional to use proper customer token validation
- Customer auth functions now return Customer object (not User)

## Shop Orders API
- Update orders.py to receive Customer directly from auth dependency
- Remove broken get_customer_from_user() helper
- Use VendorNotFoundException instead of HTTPException

## Shop Error Pages
- Fix all error templates (400, 401, 403, 404, 422, 429, 500, 502, generic)
- Templates were using undefined CSS classes (.btn, .status-code, etc.)
- Now properly extend base.html and override specific blocks
- Use Tailwind utility classes for consistent styling

## Documentation
- Update docs/api/authentication.md with new Customer return types
- Document vendor validation security features
- Update docs/api/authentication-quick-reference.md examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 22:48:02 +01:00

37 KiB

Authentication System Documentation

Version: 1.0 Last Updated: November 2025 Audience: Development Team & API Consumers


Table of Contents

  1. System Overview
  2. Architecture
  3. Authentication Contexts
  4. Implementation Guide
  5. API Reference
  6. Security Model
  7. Testing Guidelines
  8. Troubleshooting
  9. Best Practices

System Overview

The Wizamart platform uses a context-based authentication system with three isolated security domains:

  • Admin Portal - Platform administration and management
  • Vendor Portal - Multi-tenant shop management
  • Customer Shop - Public storefront and customer accounts

Each context uses dual authentication supporting both cookie-based (for HTML pages) and header-based (for API calls) authentication with complete isolation between contexts.

Key Features

  • Cookie Path Isolation - Separate cookies per context prevent cross-context access
  • Role-Based Access Control - Strict enforcement of user roles
  • JWT Token Authentication - Stateless, secure token-based auth
  • HTTP-Only Cookies - XSS protection for browser sessions
  • CSRF Protection - SameSite cookie attribute
  • Comprehensive Logging - Full audit trail of authentication events

Architecture

Authentication Flow

┌─────────────────────────────────────────────────────┐
│                    Client Request                    │
└─────────────────┬───────────────────────────────────┘
                  │
          ┌───────▼────────┐
          │  Route Handler │
          └───────┬────────┘
                  │
          ┌───────▼────────────────────────────────┐
          │  Authentication Dependency             │
          │  (from app/api/deps.py)                │
          └───────┬────────────────────────────────┘
                  │
    ┌─────────────┼─────────────┐
    │             │             │
┌───▼───┐   ┌────▼────┐   ┌───▼────┐
│Cookie │   │ Header  │   │ None   │
└───┬───┘   └────┬────┘   └───┬────┘
    │            │            │
    └────────┬───┴────────────┘
             │
      ┌──────▼───────┐
      │ Validate JWT │
      └──────┬───────┘
             │
      ┌──────▼──────────┐
      │ Check User Role │
      └──────┬──────────┘
             │
    ┌────────┴─────────┐
    │                  │
┌───▼────┐      ┌─────▼──────┐
│Success │      │ Auth Error │
│Return  │      │ 401/403    │
│User    │      └────────────┘
└────────┘

Each authentication context uses a separate cookie with path restrictions:

Context Cookie Name Cookie Path Access Scope
Admin admin_token /admin Admin routes only
Vendor vendor_token /vendor Vendor routes only
Customer customer_token /shop Shop routes only

Browser Behavior:

  • When requesting /admin/*, browser sends admin_token cookie only
  • When requesting /vendor/*, browser sends vendor_token cookie only
  • When requesting /shop/*, browser sends customer_token cookie only

This prevents cookie leakage between contexts.


Authentication Contexts

1. Admin Context

Routes: /admin/* Role: admin Cookie: admin_token (path=/admin)

Purpose: Platform administration, vendor management, system configuration.

Access Control:

  • Admin users only
  • Vendor users blocked
  • Customer users blocked

Login Endpoint:

POST /api/v1/admin/auth/login

Example Request:

curl -X POST http://localhost:8000/api/v1/admin/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'

Example Response:

{
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "user": {
        "id": 1,
        "username": "admin",
        "email": "admin@example.com",
        "role": "admin",
        "is_active": true
    }
}

Additionally, sets cookie:

Set-Cookie: admin_token=<JWT>; Path=/admin; HttpOnly; Secure; SameSite=Lax

2. Vendor Context

Routes: /vendor/* Role: vendor Cookie: vendor_token (path=/vendor)

Purpose: Vendor shop management, product catalog, orders, team management.

Access Control:

  • Admin users blocked (admins use admin portal for vendor management)
  • Vendor users (owners and team members)
  • Customer users blocked

Login Endpoint:

POST /api/v1/vendor/auth/login

Example Request:

curl -X POST http://localhost:8000/api/v1/vendor/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"vendor_owner","password":"vendor123"}'

Example Response:

{
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "user": {
        "id": 2,
        "username": "vendor_owner",
        "email": "owner@vendorshop.com",
        "role": "vendor",
        "is_active": true
    },
    "vendor": {
        "id": 1,
        "vendor_code": "ACME",
        "name": "ACME Store"
    },
    "vendor_role": "owner"
}

Additionally, sets cookie:

Set-Cookie: vendor_token=<JWT>; Path=/vendor; HttpOnly; Secure; SameSite=Lax

3. Customer Context

Routes: /shop/account/* (authenticated), /shop/* (public) Role: customer Cookie: customer_token (path=/shop)

Purpose: Product browsing (public), customer accounts, orders, profile management.

Important - URL Pattern Context: The /shop/* routes work differently depending on deployment mode:

  • Subdomain Mode (Production): https://vendor.platform.com/shop/products
  • Custom Domain (Production): https://customdomain.com/shop/products
  • Path-Based (Development): http://localhost:8000/vendors/{vendor_code}/shop/products

In path-based development mode, the full URL includes the vendor code (e.g., /vendors/acme/shop/products), but the routes are still defined as /shop/* internally. See URL Routing Guide for details.

Access Control:

  • Public Routes (/shop/products, /shop/cart, etc.):
    • Anyone can access (no authentication)
  • Account Routes (/shop/account/*):
    • Admin users blocked
    • Vendor users blocked
    • Customer users only

Login Endpoint:

POST /api/v1/public/vendors/{vendor_id}/customers/login

Example Request:

curl -X POST http://localhost:8000/api/v1/public/vendors/1/customers/login \
  -H "Content-Type: application/json" \
  -d '{"username":"customer","password":"customer123"}'

Example Response:

{
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "user": {
        "id": 100,
        "email": "customer@example.com",
        "customer_number": "CUST-001",
        "is_active": true
    }
}

Additionally, sets cookie:

Set-Cookie: customer_token=<JWT>; Path=/shop; HttpOnly; Secure; SameSite=Lax

Implementation Guide

Module Structure

app/api/
├── deps.py                    # Authentication dependencies
├── v1/
    ├── admin/
    │   └── auth.py           # Admin authentication endpoints
    ├── vendor/
    │   └── auth.py           # Vendor authentication endpoints
    └── public/vendors/
        └── auth.py           # Customer authentication endpoints

For HTML Pages (Server-Rendered)

Use the *_from_cookie_or_header functions for pages that users navigate to:

from fastapi import APIRouter, Request, Depends
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session

from app.api.deps import (
    get_current_admin_from_cookie_or_header,
    get_current_vendor_from_cookie_or_header,
    get_current_customer_from_cookie_or_header,
    get_db
)
from models.database.user import User

router = APIRouter()

# Admin page
@router.get("/admin/dashboard", response_class=HTMLResponse)
async def admin_dashboard(
    request: Request,
    current_user: User = Depends(get_current_admin_from_cookie_or_header),
    db: Session = Depends(get_db)
):
    return templates.TemplateResponse("admin/dashboard.html", {
        "request": request,
        "user": current_user
    })

# Vendor page
@router.get("/vendor/{vendor_code}/dashboard", response_class=HTMLResponse)
async def vendor_dashboard(
    request: Request,
    vendor_code: str,
    current_user: User = Depends(get_current_vendor_from_cookie_or_header),
    db: Session = Depends(get_db)
):
    return templates.TemplateResponse("vendor/dashboard.html", {
        "request": request,
        "user": current_user,
        "vendor_code": vendor_code
    })

# Customer account page
@router.get("/shop/account/dashboard", response_class=HTMLResponse)
async def customer_dashboard(
    request: Request,
    current_user: User = Depends(get_current_customer_from_cookie_or_header),
    db: Session = Depends(get_db)
):
    return templates.TemplateResponse("shop/account/dashboard.html", {
        "request": request,
        "user": current_user
    })

For API Endpoints (JSON Responses)

Use the *_api functions for API endpoints to enforce header-based authentication:

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session

from app.api.deps import (
    get_current_admin_api,
    get_current_vendor_api,
    get_current_customer_api,
    get_db
)
from models.database.user import User

router = APIRouter()

# Admin API
@router.post("/api/v1/admin/vendors")
def create_vendor(
    vendor_data: VendorCreate,
    current_user: User = Depends(get_current_admin_api),
    db: Session = Depends(get_db)
):
    # Only accepts Authorization header (no cookies)
    # Better security - prevents CSRF attacks
    return {"message": "Vendor created"}

# Vendor API
@router.post("/api/v1/vendor/{vendor_code}/products")
def create_product(
    vendor_code: str,
    product_data: ProductCreate,
    current_user: User = Depends(get_current_vendor_api),
    db: Session = Depends(get_db)
):
    return {"message": "Product created"}

# Customer API
@router.post("/api/v1/shop/orders")
def create_order(
    order_data: OrderCreate,
    current_user: User = Depends(get_current_customer_api),
    db: Session = Depends(get_db)
):
    return {"message": "Order created"}

For Public Routes (No Authentication)

Simply don't use any authentication dependency:

@router.get("/shop/products")
async def public_products(request: Request):
    # No authentication required
    return templates.TemplateResponse("shop/products.html", {
        "request": request
    })

API Reference

Authentication Dependencies

All authentication functions are in app/api/deps.py:

Purpose: Authenticate admin users for HTML pages Accepts: Cookie (admin_token) OR Authorization header Returns: User object with role="admin" Raises:

  • InvalidTokenException - No token or invalid token
  • AdminRequiredException - User is not admin

Usage:

current_user: User = Depends(get_current_admin_from_cookie_or_header)

get_current_admin_api()

Purpose: Authenticate admin users for API endpoints Accepts: Authorization header ONLY Returns: User object with role="admin" Raises:

  • InvalidTokenException - No token or invalid token
  • AdminRequiredException - User is not admin

Usage:

current_user: User = Depends(get_current_admin_api)

Purpose: Authenticate vendor users for HTML pages Accepts: Cookie (vendor_token) OR Authorization header Returns: User object with role="vendor" Raises:

  • InvalidTokenException - No token or invalid token
  • InsufficientPermissionsException - User is not vendor or is admin

Note: The InsufficientPermissionsException raised here is from app.exceptions.auth, which provides general authentication permission checking. This is distinct from InsufficientTeamPermissionsException used for team-specific permissions.

Usage:

current_user: User = Depends(get_current_vendor_from_cookie_or_header)

get_current_vendor_api()

Purpose: Authenticate vendor users for API endpoints Accepts: Authorization header ONLY Returns: User object with role="vendor" Raises:

  • InvalidTokenException - No token or invalid token
  • InsufficientPermissionsException - User is not vendor or is admin

Usage:

current_user: User = Depends(get_current_vendor_api)

Purpose: Authenticate customer users for HTML pages Accepts: Cookie (customer_token) OR Authorization header Returns: Customer object (from models.database.customer.Customer) Raises:

  • InvalidTokenException - No token, invalid token, or not a customer token
  • UnauthorizedVendorAccessException - Token vendor_id doesn't match URL vendor

Security Features:

  • Token type validation: Only accepts tokens with type: "customer" - admin and vendor tokens are rejected
  • Vendor validation: Validates that token.vendor_id matches request.state.vendor.id (URL-based vendor)
  • Prevents cross-vendor token reuse (customer from Vendor A cannot use token on Vendor B's shop)

Usage:

from models.database.customer import Customer

customer: Customer = Depends(get_current_customer_from_cookie_or_header)
# Access customer.id, customer.email, customer.vendor_id, etc.

get_current_customer_api()

Purpose: Authenticate customer users for API endpoints Accepts: Authorization header ONLY Returns: Customer object (from models.database.customer.Customer) Raises:

  • InvalidTokenException - No token, invalid token, or not a customer token
  • UnauthorizedVendorAccessException - Token vendor_id doesn't match URL vendor

Security Features:

  • Token type validation: Only accepts tokens with type: "customer" - admin and vendor tokens are rejected
  • Vendor validation: Validates that token.vendor_id matches request.state.vendor.id (URL-based vendor)
  • Prevents cross-vendor token reuse

Usage:

from models.database.customer import Customer

@router.post("/orders")
def place_order(
    request: Request,
    customer: Customer = Depends(get_current_customer_api),
    db: Session = Depends(get_db),
):
    # customer is a Customer object, not User
    vendor = request.state.vendor  # Already validated to match token
    order = order_service.create_order(db, vendor.id, customer.id, ...)

get_current_user()

Purpose: Authenticate any user (no role checking) Accepts: Authorization header ONLY Returns: User object (any role) Raises:

  • InvalidTokenException - No token or invalid token

Usage:

current_user: User = Depends(get_current_user)

Optional Authentication Dependencies

These dependencies return None instead of raising exceptions when authentication fails. Used for login pages and public routes that need to conditionally check if a user is authenticated.

get_current_admin_optional()

Purpose: Check if admin user is authenticated (without enforcing) Accepts: Cookie (admin_token) OR Authorization header Returns:

  • User object with role="admin" if authenticated
  • None if no token, invalid token, or user is not admin Raises: Never raises exceptions

Usage:

# Login page redirect
@router.get("/admin/login")
async def admin_login_page(
    request: Request,
    current_user: Optional[User] = Depends(get_current_admin_optional)
):
    if current_user:
        # User already logged in, redirect to dashboard
        return RedirectResponse(url="/admin/dashboard", status_code=302)

    # Not logged in, show login form
    return templates.TemplateResponse("admin/login.html", {"request": request})

Use Cases:

  • Login pages (redirect if already authenticated)
  • Public pages with conditional admin content
  • Root redirects based on authentication status

get_current_vendor_optional()

Purpose: Check if vendor user is authenticated (without enforcing) Accepts: Cookie (vendor_token) OR Authorization header Returns:

  • User object with role="vendor" if authenticated
  • None if no token, invalid token, or user is not vendor Raises: Never raises exceptions

Usage:

# Login page redirect
@router.get("/vendor/{vendor_code}/login")
async def vendor_login_page(
    request: Request,
    vendor_code: str = Path(...),
    current_user: Optional[User] = Depends(get_current_vendor_optional)
):
    if current_user:
        # User already logged in, redirect to dashboard
        return RedirectResponse(url=f"/vendor/{vendor_code}/dashboard", status_code=302)

    # Not logged in, show login form
    return templates.TemplateResponse("vendor/login.html", {
        "request": request,
        "vendor_code": vendor_code
    })

Use Cases:

  • Login pages (redirect if already authenticated)
  • Public vendor pages with conditional content
  • Root redirects based on authentication status

get_current_customer_optional()

Purpose: Check if customer user is authenticated (without enforcing) Accepts: Cookie (customer_token) OR Authorization header Returns:

  • Customer object if authenticated with valid customer token
  • None if no token, invalid token, vendor mismatch, or not a customer token Raises: Never raises exceptions

Security Features:

  • Only accepts tokens with type: "customer" - admin and vendor tokens return None
  • Validates vendor_id in token matches URL vendor - mismatch returns None

Usage:

from models.database.customer import Customer

# Shop login page redirect
@router.get("/shop/account/login")
async def customer_login_page(
    request: Request,
    customer: Customer | None = Depends(get_current_customer_optional)
):
    if customer:
        # Customer already logged in, redirect to account page
        return RedirectResponse(url="/shop/account", status_code=302)

    # Not logged in, show login form
    return templates.TemplateResponse("shop/login.html", {"request": request})

Use Cases:

  • Login pages (redirect if already authenticated)
  • Public shop pages with conditional customer content (e.g., "My Orders" link)
  • Root redirects based on authentication status

Required vs Optional Dependencies

Understanding when to use each type:

Admin Context

Scenario Use Returns On Auth Failure
Protected page (dashboard, settings) get_current_admin_from_cookie_or_header User Raises 401 exception
Login page get_current_admin_optional Optional[User] Returns None
API endpoint get_current_admin_api User Raises 401 exception
Public page with conditional content get_current_admin_optional Optional[User] Returns None

Vendor Context

Scenario Use Returns On Auth Failure
Protected page (dashboard, products) get_current_vendor_from_cookie_or_header User Raises 401 exception
Login page get_current_vendor_optional Optional[User] Returns None
API endpoint get_current_vendor_api User Raises 401 exception
Public page with conditional content get_current_vendor_optional Optional[User] Returns None

Customer Context

Scenario Use Returns On Auth Failure
Protected page (account, orders) get_current_customer_from_cookie_or_header User Raises 401 exception
Login page get_current_customer_optional Optional[User] Returns None
API endpoint get_current_customer_api User Raises 401 exception
Public page with conditional content get_current_customer_optional Optional[User] Returns None

Example Flow:

# ❌ WRONG: Using required auth on login page
@router.get("/admin/login")
async def admin_login_page(
    current_user: User = Depends(get_current_admin_from_cookie_or_header)
):
    # This will return 401 error if not logged in!
    # Login page will never render for unauthenticated users
    ...

# ✅ CORRECT: Using optional auth on login page
@router.get("/admin/login")
async def admin_login_page(
    current_user: Optional[User] = Depends(get_current_admin_optional)
):
    # Returns None if not logged in, page renders normally
    if current_user:
        return RedirectResponse(url="/admin/dashboard")
    return templates.TemplateResponse("login.html", ...)


# ✅ CORRECT: Using required auth on protected page
@router.get("/admin/dashboard")
async def admin_dashboard(
    current_user: User = Depends(get_current_admin_from_cookie_or_header)
):
    # Automatically returns 401 if not authenticated
    # Only runs if user is authenticated admin
    ...

Login Response Format

All login endpoints return:

{
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "user": {
        "id": 1,
        "username": "admin",
        "email": "admin@example.com",
        "role": "admin",
        "is_active": true
    }
}

Additionally, the response sets an HTTP-only cookie:

  • Admin: admin_token (path=/admin)
  • Vendor: vendor_token (path=/vendor)
  • Customer: customer_token (path=/shop)

Security Model

Role-Based Access Control Matrix

User Role Admin Portal Vendor Portal Shop Catalog Customer Account
Admin Full Blocked View Blocked
Vendor Blocked Full View Blocked
Customer Blocked Blocked View Full
Anonymous Blocked Blocked View Blocked

All authentication cookies use the following security attributes:

response.set_cookie(
    key="<context>_token",
    value=jwt_token,
    httponly=True,        # JavaScript cannot access (XSS protection)
    secure=True,          # HTTPS only in production
    samesite="lax",       # CSRF protection
    max_age=3600,         # Matches JWT expiry
    path="/<context>"     # Path restriction for isolation
)

Token Validation

JWT tokens include:

  • sub - User ID
  • role - User role (admin/vendor/customer)
  • exp - Expiration timestamp
  • iat - Issued at timestamp

Tokens are validated on every request:

  1. Extract token from cookie or header
  2. Verify JWT signature
  3. Check expiration
  4. Load user from database
  5. Verify user is active
  6. Check role matches route requirements

Token Validation Edge Cases

The token verification process includes comprehensive validation of token claims:

Required Claims Validation:

  • Missing sub (User ID): Raises InvalidTokenException("Token missing user identifier")
  • Missing exp (Expiration): Raises InvalidTokenException("Token missing expiration")
  • Expired Token: Raises TokenExpiredException()

Signature Verification:

  • Invalid Signature: Raises InvalidTokenException("Could not validate credentials")
  • Wrong Algorithm: Raises InvalidTokenException()
  • Malformed Token: Raises InvalidTokenException()

Exception Handling Pattern: Custom exceptions (such as those raised for missing claims) are preserved with their specific error messages, allowing for detailed error reporting to clients. This follows the exception handling pattern documented in the Exception Handling Guide.

Example Error Responses:

{
  "error_code": "INVALID_TOKEN",
  "message": "Token missing user identifier",
  "status_code": 401
}
{
  "error_code": "TOKEN_EXPIRED",
  "message": "Token has expired",
  "status_code": 401
}

HTTPS Requirement

Production Environment:

  • All cookies have secure=True
  • HTTPS required for all authenticated routes
  • HTTP requests automatically redirect to HTTPS

Development Environment:


Testing Guidelines

Manual Testing with Browser

Test Admin Authentication

  1. Navigate to admin login:

    http://localhost:8000/admin/login
    
  2. Login with admin credentials:

    • Username: admin
    • Password: admin123 (or your configured admin password)
  3. Verify cookie in DevTools:

    • Open DevTools → Application → Cookies
    • Look for admin_token cookie
    • Verify Path is /admin
    • Verify HttpOnly is checked
    • Verify SameSite is Lax
  4. Test navigation:

    • Navigate to /admin/dashboard - Should work
    • Navigate to /vendor/TESTVENDOR/dashboard - Should fail (cookie not sent)
    • Navigate to /shop/account/dashboard - Should fail (cookie not sent)
  5. Logout:

    POST /api/v1/admin/auth/logout
    

Test Vendor Authentication

  1. Navigate to vendor login:

    http://localhost:8000/vendor/{VENDOR_CODE}/login
    
  2. Login with vendor credentials

  3. Verify cookie in DevTools:

    • Look for vendor_token cookie
    • Verify Path is /vendor
  4. Test navigation:

    • Navigate to /vendor/{VENDOR_CODE}/dashboard - Should work
    • Navigate to /admin/dashboard - Should fail
    • Navigate to /shop/account/dashboard - Should fail

Test Customer Authentication

  1. Navigate to customer login:

    http://localhost:8000/shop/account/login
    
  2. Login with customer credentials

  3. Verify cookie in DevTools:

    • Look for customer_token cookie
    • Verify Path is /shop
  4. Test navigation:

    • Navigate to /shop/account/dashboard - Should work
    • Navigate to /admin/dashboard - Should fail
    • Navigate to /vendor/{CODE}/dashboard - Should fail

API Testing with curl

Test Admin API

# Login
curl -X POST http://localhost:8000/api/v1/admin/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'

# Save the access_token from response

# Test authenticated endpoint
curl http://localhost:8000/api/v1/admin/vendors \
  -H "Authorization: Bearer <access_token>"

# Test cross-context blocking
curl http://localhost:8000/api/v1/vendor/TESTVENDOR/products \
  -H "Authorization: Bearer <admin_access_token>"
# Should return 403 Forbidden

Test Vendor API

# Login
curl -X POST http://localhost:8000/api/v1/vendor/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"vendor","password":"vendor123"}'

# Test authenticated endpoint
curl http://localhost:8000/api/v1/vendor/TESTVENDOR/products \
  -H "Authorization: Bearer <vendor_access_token>"

# Test cross-context blocking
curl http://localhost:8000/api/v1/admin/vendors \
  -H "Authorization: Bearer <vendor_access_token>"
# Should return 403 Forbidden

Test Customer API

# Login
curl -X POST http://localhost:8000/api/v1/public/vendors/1/customers/login \
  -H "Content-Type: application/json" \
  -d '{"username":"customer","password":"customer123"}'

# Test authenticated endpoint with token
curl http://localhost:8000/api/v1/shop/orders \
  -H "Authorization: Bearer <customer_access_token>"

# Test cross-context blocking
curl http://localhost:8000/api/v1/admin/vendors \
  -H "Authorization: Bearer <customer_access_token>"
# Should return 403 Forbidden

Frontend JavaScript Testing

Login and Store Token

// Admin login
async function loginAdmin(username, password) {
    const response = await fetch('/api/v1/admin/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });

    const data = await response.json();

    // Cookie is set automatically
    // Optionally store token for API calls
    localStorage.setItem('admin_token', data.access_token);

    // Redirect to dashboard
    window.location.href = '/admin/dashboard';
}

Make API Call with Token

// API call with token
async function fetchVendors() {
    const token = localStorage.getItem('admin_token');

    const response = await fetch('/api/v1/admin/vendors', {
        headers: {
            'Authorization': `Bearer ${token}`
        }
    });

    return response.json();
}
// Just navigate - cookie sent automatically
window.location.href = '/admin/dashboard';
// Browser automatically includes admin_token cookie

Automated Testing

import pytest
from fastapi.testclient import TestClient

def test_admin_cookie_not_sent_to_vendor_routes(client: TestClient):
    # Login as admin
    response = client.post('/api/v1/admin/auth/login', json={
        'username': 'admin',
        'password': 'admin123'
    })

    # Try to access vendor route (cookie should not be sent)
    response = client.get('/vendor/TESTVENDOR/dashboard')

    # Should redirect to login or return 401
    assert response.status_code in [302, 401]

def test_vendor_token_blocked_from_admin_api(client: TestClient):
    # Login as vendor
    response = client.post('/api/v1/vendor/auth/login', json={
        'username': 'vendor',
        'password': 'vendor123'
    })
    vendor_token = response.json()['access_token']

    # Try to access admin API with vendor token
    response = client.get(
        '/api/v1/admin/vendors',
        headers={'Authorization': f'Bearer {vendor_token}'}
    )

    # Should return 403 Forbidden
    assert response.status_code == 403

Troubleshooting

Common Issues

"Invalid token" error when navigating to pages

Symptom: User is logged in but gets "Invalid token" error

Causes:

  • Token expired (default: 1 hour)
  • Cookie was deleted
  • Wrong cookie being sent

Solution:

  • Check cookie expiration in DevTools
  • Re-login to get fresh token
  • Verify correct cookie exists with correct path

Symptom: API calls work with Authorization header but pages don't load

Causes:

  • Cookie path mismatch
  • Cookie expired
  • Wrong domain

Solution:

  • Verify cookie path matches route (e.g., /admin cookie for /admin/* routes)
  • Check cookie expiration
  • Ensure cookie domain matches current domain

"Admin cannot access vendor portal" error

Symptom: Admin user cannot access vendor routes

Explanation: This is intentional security design. Admins have their own portal at /admin. To manage vendors, use admin routes:

  • View vendors: /admin/vendors
  • Edit vendor: /admin/vendors/{code}/edit

Admins should not log into vendor portal as this violates security boundaries.

"Customer cannot access admin/vendor routes" error

Symptom: Customer trying to access management interfaces

Explanation: Customers only have access to:

  • Public shop routes: /shop/products, etc.
  • Their account: /shop/account/*

Admin and vendor portals are not accessible to customers.

Token works in Postman but not in browser

Cause: Postman uses Authorization header, browser uses cookies

Solution:

  • For API testing: Use Authorization header
  • For browser testing: Rely on cookies (automatic)
  • For JavaScript API calls: Add Authorization header manually

Debugging Tips

// In browser console
document.cookie.split(';').forEach(c => console.log(c.trim()));

Decode JWT Token

// In browser console
function parseJwt(token) {
    const base64Url = token.split('.')[1];
    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
    return JSON.parse(jsonPayload);
}

const token = localStorage.getItem('admin_token');
console.log(parseJwt(token));

Check Server Logs

The authentication system logs all auth events:

INFO: Admin login successful: admin
INFO: Request: GET /admin/dashboard from 127.0.0.1
INFO: Response: 200 for GET /admin/dashboard (0.045s)

Look for:

  • Login attempts
  • Token validation errors
  • Permission denials

Best Practices

For Developers

  1. Use the right dependency for the job:

    • HTML pages → get_current_<context>_from_cookie_or_header
    • API endpoints → get_current_<context>_api
  2. Don't mix authentication contexts:

    • Admin users should use admin portal
    • Vendor users should use vendor portal
    • Customers should use shop
  3. Always check user.is_active:

    if not current_user.is_active:
        raise UserNotActiveException()
    
  4. Use type hints:

    def my_route(current_user: User = Depends(get_current_admin_api)):
        # IDE will have autocomplete for current_user
    
  5. Handle exceptions properly:

    try:
        # Your logic
    except InvalidTokenException:
        # Handle auth failure
    except InsufficientPermissionsException:
        # Handle permission denial
    

For Frontend

  1. Store tokens securely:

    • Tokens in localStorage/sessionStorage are vulnerable to XSS
    • Prefer using cookies for page navigation
    • Only use localStorage for explicit API calls
  2. Always send Authorization header for API calls:

    const token = localStorage.getItem('token');
    fetch('/api/v1/admin/vendors', {
        headers: { 'Authorization': `Bearer ${token}` }
    });
    
  3. Handle 401/403 responses:

    if (response.status === 401) {
        // Redirect to login
        window.location.href = '/admin/login';
    }
    
  4. Clear tokens on logout:

    localStorage.removeItem('token');
    // Logout endpoint will clear cookie
    await fetch('/api/v1/admin/auth/logout', { method: 'POST' });
    

Security Considerations

  1. Never log tokens - They're sensitive credentials
  2. Use HTTPS in production - Required for secure cookies
  3. Set appropriate token expiration - Balance security vs UX
  4. Rotate secrets regularly - JWT signing keys
  5. Monitor failed auth attempts - Detect brute force attacks

Configuration

Environment Variables

# JWT Configuration
JWT_SECRET_KEY=your-secret-key-here
JWT_ALGORITHM=HS256
JWT_EXPIRATION=3600  # 1 hour in seconds

# Environment
ENVIRONMENT=production  # or development

# When ENVIRONMENT=production:
# - Cookies use secure=True (HTTPS only)
# - Debug mode is disabled
# - CORS is stricter

Cookies expire when:

  1. JWT token expires (default: 1 hour)
  2. User logs out (cookie deleted)
  3. Browser session ends (for session cookies)

To change expiration:

# In auth endpoint
response.set_cookie(
    max_age=7200  # 2 hours
)

AuthManager Class Reference

The AuthManager class handles all authentication and authorization operations including password hashing, JWT token management, and role-based access control.

::: middleware.auth.AuthManager options: show_source: false heading_level: 3 show_root_heading: true show_root_toc_entry: false members: - init - hash_password - verify_password - authenticate_user - create_access_token - verify_token - get_current_user - require_role - require_admin - require_vendor - require_customer - create_default_admin_user


Quick Reference

For a condensed cheat sheet of authentication patterns, see Authentication Quick Reference.



Document Version: 1.0 Last Updated: November 2025 Maintained By: Backend Team