Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1316 lines
38 KiB
Markdown
1316 lines
38 KiB
Markdown
# Authentication System Documentation
|
|
|
|
**Version:** 1.0
|
|
**Last Updated:** November 2025
|
|
**Audience:** Development Team & API Consumers
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [System Overview](#system-overview)
|
|
2. [Architecture](#architecture)
|
|
3. [Authentication Contexts](#authentication-contexts)
|
|
4. [Implementation Guide](#implementation-guide)
|
|
5. [API Reference](#api-reference)
|
|
6. [Security Model](#security-model)
|
|
7. [Testing Guidelines](#testing-guidelines)
|
|
8. [Troubleshooting](#troubleshooting)
|
|
9. [Best Practices](#best-practices)
|
|
|
|
---
|
|
|
|
## System Overview
|
|
|
|
The Orion platform uses a **context-based authentication system** with three isolated security domains:
|
|
|
|
- **Admin Portal** - Platform administration and management
|
|
- **Store 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 │ └────────────┘
|
|
└────────┘
|
|
```
|
|
|
|
### Cookie Isolation
|
|
|
|
Each authentication context uses a separate cookie with path restrictions:
|
|
|
|
| Context | Cookie Name | Cookie Path | Access Scope |
|
|
|----------|------------------|-------------|--------------|
|
|
| Admin | `admin_token` | `/admin` | Admin routes only |
|
|
| Store | `store_token` | `/store` | Store routes only |
|
|
| Customer | `customer_token` | `/shop` | Shop routes only |
|
|
|
|
**Browser Behavior:**
|
|
- When requesting `/admin/*`, browser sends `admin_token` cookie only
|
|
- When requesting `/store/*`, browser sends `store_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, store management, system configuration.
|
|
|
|
**Access Control:**
|
|
- ✅ Admin users only
|
|
- ❌ Store users blocked
|
|
- ❌ Customer users blocked
|
|
|
|
**Login Endpoint:**
|
|
```
|
|
POST /api/v1/admin/auth/login
|
|
```
|
|
|
|
**Example Request:**
|
|
```bash
|
|
curl -X POST http://localhost:8000/api/v1/admin/auth/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"admin","password":"admin123"}'
|
|
```
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"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. Store Context
|
|
|
|
**Routes:** `/store/*`
|
|
**Role:** `store`
|
|
**Cookie:** `store_token` (path=/store)
|
|
|
|
**Purpose:** Store shop management, product catalog, orders, team management.
|
|
|
|
**Access Control:**
|
|
- ❌ Admin users blocked (admins use admin portal for store management)
|
|
- ✅ Store users (owners and team members)
|
|
- ❌ Customer users blocked
|
|
|
|
**Login Endpoint:**
|
|
```
|
|
POST /api/v1/store/auth/login
|
|
```
|
|
|
|
**Example Request:**
|
|
```bash
|
|
curl -X POST http://localhost:8000/api/v1/store/auth/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"store_owner","password":"store123"}'
|
|
```
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
"user": {
|
|
"id": 2,
|
|
"username": "store_owner",
|
|
"email": "owner@storeshop.com",
|
|
"role": "store",
|
|
"is_active": true
|
|
},
|
|
"store": {
|
|
"id": 1,
|
|
"store_code": "ACME",
|
|
"name": "ACME Store"
|
|
},
|
|
"store_role": "owner"
|
|
}
|
|
```
|
|
|
|
Additionally, sets cookie:
|
|
```
|
|
Set-Cookie: store_token=<JWT>; Path=/store; 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://store.platform.com/shop/products`
|
|
- **Custom Domain** (Production): `https://customdomain.com/shop/products`
|
|
- **Path-Based** (Development): `http://localhost:8000/stores/{store_code}/shop/products`
|
|
|
|
In path-based development mode, the full URL includes the store code (e.g., `/stores/acme/shop/products`), but the routes are still defined as `/shop/*` internally. See [URL Routing Guide](../architecture/url-routing/overview.md) for details.
|
|
|
|
**Access Control:**
|
|
- **Public Routes** (`/shop/products`, `/shop/cart`, etc.):
|
|
- ✅ Anyone can access (no authentication)
|
|
- **Account Routes** (`/shop/account/*`):
|
|
- ❌ Admin users blocked
|
|
- ❌ Store users blocked
|
|
- ✅ Customer users only
|
|
|
|
**Login Endpoint:**
|
|
```
|
|
POST /api/v1/platform/stores/{store_id}/customers/login
|
|
```
|
|
|
|
**Example Request:**
|
|
```bash
|
|
curl -X POST http://localhost:8000/api/v1/platform/stores/1/customers/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"customer","password":"customer123"}'
|
|
```
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"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
|
|
├── store/
|
|
│ └── auth.py # Store authentication endpoints
|
|
└── public/stores/
|
|
└── auth.py # Customer authentication endpoints
|
|
```
|
|
|
|
### For HTML Pages (Server-Rendered)
|
|
|
|
Use the `*_from_cookie_or_header` functions for pages that users navigate to:
|
|
|
|
```python
|
|
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_store_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
|
|
})
|
|
|
|
# Store page
|
|
@router.get("/store/{store_code}/dashboard", response_class=HTMLResponse)
|
|
async def store_dashboard(
|
|
request: Request,
|
|
store_code: str,
|
|
current_user: User = Depends(get_current_store_from_cookie_or_header),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
return templates.TemplateResponse("store/dashboard.html", {
|
|
"request": request,
|
|
"user": current_user,
|
|
"store_code": store_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:
|
|
|
|
```python
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import (
|
|
get_current_admin_api,
|
|
get_current_store_api,
|
|
get_current_customer_api,
|
|
get_db
|
|
)
|
|
from models.database.user import User
|
|
|
|
router = APIRouter()
|
|
|
|
# Admin API
|
|
@router.post("/api/v1/admin/stores")
|
|
def create_store(
|
|
store_data: StoreCreate,
|
|
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": "Store created"}
|
|
|
|
# Store API
|
|
@router.post("/api/v1/store/{store_code}/products")
|
|
def create_product(
|
|
store_code: str,
|
|
product_data: ProductCreate,
|
|
current_user: User = Depends(get_current_store_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:
|
|
|
|
```python
|
|
@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`:
|
|
|
|
#### `get_current_admin_from_cookie_or_header()`
|
|
|
|
**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:**
|
|
```python
|
|
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:**
|
|
```python
|
|
current_user: User = Depends(get_current_admin_api)
|
|
```
|
|
|
|
#### `get_current_store_from_cookie_or_header()`
|
|
|
|
**Purpose:** Authenticate store users for HTML pages
|
|
**Accepts:** Cookie (`store_token`) OR Authorization header
|
|
**Returns:** `User` object with `role="store"`
|
|
**Raises:**
|
|
- `InvalidTokenException` - No token or invalid token
|
|
- `InsufficientPermissionsException` - User is not store 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:**
|
|
```python
|
|
current_user: User = Depends(get_current_store_from_cookie_or_header)
|
|
```
|
|
|
|
#### `get_current_store_api()`
|
|
|
|
**Purpose:** Authenticate store users for API endpoints
|
|
**Accepts:** Authorization header ONLY
|
|
**Returns:** `User` object with `role="store"` and **guaranteed** attributes:
|
|
- `current_user.token_store_id` - Store ID from JWT token
|
|
- `current_user.token_store_code` - Store code from JWT token
|
|
- `current_user.token_store_role` - User's role in store (owner, manager, etc.)
|
|
|
|
**Raises:**
|
|
- `InvalidTokenException` - No token, invalid token, or **missing store context in token**
|
|
- `InsufficientPermissionsException` - User is not store, is admin, or lost access to store
|
|
|
|
**Guarantees:**
|
|
This dependency **guarantees** that `token_store_id` is present. Endpoints should NOT check for its existence:
|
|
|
|
```python
|
|
# ❌ WRONG - Redundant check violates API-003
|
|
if not hasattr(current_user, "token_store_id"):
|
|
raise InvalidTokenException("...")
|
|
|
|
# ✅ CORRECT - Dependency guarantees this attribute exists
|
|
store_id = current_user.token_store_id
|
|
```
|
|
|
|
**Usage:**
|
|
```python
|
|
@router.get("/orders")
|
|
def get_orders(
|
|
current_user: User = Depends(get_current_store_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
# Safe to use directly - dependency guarantees token_store_id
|
|
orders = order_service.get_store_orders(db, current_user.token_store_id)
|
|
return orders
|
|
```
|
|
|
|
#### `get_current_customer_from_cookie_or_header()`
|
|
|
|
**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
|
|
- `UnauthorizedStoreAccessException` - Token store_id doesn't match URL store
|
|
|
|
**Security Features:**
|
|
- **Token type validation:** Only accepts tokens with `type: "customer"` - admin and store tokens are rejected
|
|
- **Store validation:** Validates that `token.store_id` matches `request.state.store.id` (URL-based store)
|
|
- Prevents cross-store token reuse (customer from Store A cannot use token on Store B's shop)
|
|
|
|
**Usage:**
|
|
```python
|
|
from models.database.customer import Customer
|
|
|
|
customer: Customer = Depends(get_current_customer_from_cookie_or_header)
|
|
# Access customer.id, customer.email, customer.store_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
|
|
- `UnauthorizedStoreAccessException` - Token store_id doesn't match URL store
|
|
|
|
**Security Features:**
|
|
- **Token type validation:** Only accepts tokens with `type: "customer"` - admin and store tokens are rejected
|
|
- **Store validation:** Validates that `token.store_id` matches `request.state.store.id` (URL-based store)
|
|
- Prevents cross-store token reuse
|
|
|
|
**Usage:**
|
|
```python
|
|
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
|
|
store = request.state.store # Already validated to match token
|
|
order = order_service.create_order(db, store.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:**
|
|
```python
|
|
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:**
|
|
```python
|
|
# 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_store_optional()`
|
|
|
|
**Purpose:** Check if store user is authenticated (without enforcing)
|
|
**Accepts:** Cookie (`store_token`) OR Authorization header
|
|
**Returns:**
|
|
- `User` object with `role="store"` if authenticated
|
|
- `None` if no token, invalid token, or user is not store
|
|
**Raises:** Never raises exceptions
|
|
|
|
**Usage:**
|
|
```python
|
|
# Login page redirect
|
|
@router.get("/store/{store_code}/login")
|
|
async def store_login_page(
|
|
request: Request,
|
|
store_code: str = Path(...),
|
|
current_user: Optional[User] = Depends(get_current_store_optional)
|
|
):
|
|
if current_user:
|
|
# User already logged in, redirect to dashboard
|
|
return RedirectResponse(url=f"/store/{store_code}/dashboard", status_code=302)
|
|
|
|
# Not logged in, show login form
|
|
return templates.TemplateResponse("store/login.html", {
|
|
"request": request,
|
|
"store_code": store_code
|
|
})
|
|
```
|
|
|
|
**Use Cases:**
|
|
- Login pages (redirect if already authenticated)
|
|
- Public store 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, store mismatch, or not a customer token
|
|
**Raises:** Never raises exceptions
|
|
|
|
**Security Features:**
|
|
- Only accepts tokens with `type: "customer"` - admin and store tokens return `None`
|
|
- Validates store_id in token matches URL store - mismatch returns `None`
|
|
|
|
**Usage:**
|
|
```python
|
|
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` |
|
|
|
|
#### Store Context
|
|
|
|
| Scenario | Use | Returns | On Auth Failure |
|
|
|----------|-----|---------|-----------------|
|
|
| **Protected page (dashboard, products)** | `get_current_store_from_cookie_or_header` | `User` | Raises 401 exception |
|
|
| **Login page** | `get_current_store_optional` | `Optional[User]` | Returns `None` |
|
|
| **API endpoint** | `get_current_store_api` | `User` | Raises 401 exception |
|
|
| **Public page with conditional content** | `get_current_store_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:**
|
|
|
|
```python
|
|
# ❌ 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:
|
|
|
|
```python
|
|
{
|
|
"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)
|
|
- Store: `store_token` (path=/store)
|
|
- Customer: `customer_token` (path=/shop)
|
|
|
|
---
|
|
|
|
## Security Model
|
|
|
|
### Role-Based Access Control Matrix
|
|
|
|
| User Role | Admin Portal | Store Portal | Shop Catalog | Customer Account |
|
|
|-----------|--------------|---------------|--------------|------------------|
|
|
| Admin | ✅ Full | ❌ Blocked | ✅ View | ❌ Blocked |
|
|
| Store | ❌ Blocked | ✅ Full | ✅ View | ❌ Blocked |
|
|
| Customer | ❌ Blocked | ❌ Blocked | ✅ View | ✅ Full |
|
|
| Anonymous | ❌ Blocked | ❌ Blocked | ✅ View | ❌ Blocked |
|
|
|
|
### Cookie Security Settings
|
|
|
|
All authentication cookies use the following security attributes:
|
|
|
|
```python
|
|
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/store/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](../development/exception-handling.md).
|
|
|
|
**Example Error Responses:**
|
|
```json
|
|
{
|
|
"error_code": "INVALID_TOKEN",
|
|
"message": "Token missing user identifier",
|
|
"status_code": 401
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"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:**
|
|
- Cookies have `secure=False` for local testing
|
|
- HTTP allowed (http://localhost:8000)
|
|
|
|
---
|
|
|
|
## 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 `/store/TESTSTORE/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 Store Authentication
|
|
|
|
1. **Navigate to store login:**
|
|
```
|
|
http://localhost:8000/store/{STORE_CODE}/login
|
|
```
|
|
|
|
2. **Login with store credentials**
|
|
|
|
3. **Verify cookie in DevTools:**
|
|
- Look for `store_token` cookie
|
|
- Verify `Path` is `/store`
|
|
|
|
4. **Test navigation:**
|
|
- Navigate to `/store/{STORE_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 `/store/{CODE}/dashboard` - Should fail ❌
|
|
|
|
### API Testing with curl
|
|
|
|
#### Test Admin API
|
|
|
|
```bash
|
|
# 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/stores \
|
|
-H "Authorization: Bearer <access_token>"
|
|
|
|
# Test cross-context blocking
|
|
curl http://localhost:8000/api/v1/store/TESTSTORE/products \
|
|
-H "Authorization: Bearer <admin_access_token>"
|
|
# Should return 403 Forbidden
|
|
```
|
|
|
|
#### Test Store API
|
|
|
|
```bash
|
|
# Login
|
|
curl -X POST http://localhost:8000/api/v1/store/auth/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"store","password":"store123"}'
|
|
|
|
# Test authenticated endpoint
|
|
curl http://localhost:8000/api/v1/store/TESTSTORE/products \
|
|
-H "Authorization: Bearer <store_access_token>"
|
|
|
|
# Test cross-context blocking
|
|
curl http://localhost:8000/api/v1/admin/stores \
|
|
-H "Authorization: Bearer <store_access_token>"
|
|
# Should return 403 Forbidden
|
|
```
|
|
|
|
#### Test Customer API
|
|
|
|
```bash
|
|
# Login
|
|
curl -X POST http://localhost:8000/api/v1/platform/stores/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/stores \
|
|
-H "Authorization: Bearer <customer_access_token>"
|
|
# Should return 403 Forbidden
|
|
```
|
|
|
|
### Frontend JavaScript Testing
|
|
|
|
#### Login and Store Token
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// API call with token
|
|
async function fetchStores() {
|
|
const token = localStorage.getItem('admin_token');
|
|
|
|
const response = await fetch('/api/v1/admin/stores', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
return response.json();
|
|
}
|
|
```
|
|
|
|
#### Page Navigation (Cookie Automatic)
|
|
|
|
```javascript
|
|
// Just navigate - cookie sent automatically
|
|
window.location.href = '/admin/dashboard';
|
|
// Browser automatically includes admin_token cookie
|
|
```
|
|
|
|
### Automated Testing
|
|
|
|
#### Test Cookie Isolation
|
|
|
|
```python
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
def test_admin_cookie_not_sent_to_store_routes(client: TestClient):
|
|
# Login as admin
|
|
response = client.post('/api/v1/admin/auth/login', json={
|
|
'username': 'admin',
|
|
'password': 'admin123'
|
|
})
|
|
|
|
# Try to access store route (cookie should not be sent)
|
|
response = client.get('/store/TESTSTORE/dashboard')
|
|
|
|
# Should redirect to login or return 401
|
|
assert response.status_code in [302, 401]
|
|
|
|
def test_store_token_blocked_from_admin_api(client: TestClient):
|
|
# Login as store
|
|
response = client.post('/api/v1/store/auth/login', json={
|
|
'username': 'store',
|
|
'password': 'store123'
|
|
})
|
|
store_token = response.json()['access_token']
|
|
|
|
# Try to access admin API with store token
|
|
response = client.get(
|
|
'/api/v1/admin/stores',
|
|
headers={'Authorization': f'Bearer {store_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
|
|
|
|
#### Cookie not being sent to endpoints
|
|
|
|
**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 store portal" error
|
|
|
|
**Symptom:** Admin user cannot access store routes
|
|
|
|
**Explanation:** This is intentional security design. Admins have their own portal at `/admin`. To manage stores, use admin routes:
|
|
- View stores: `/admin/stores`
|
|
- Edit store: `/admin/stores/{code}/edit`
|
|
|
|
Admins should not log into store portal as this violates security boundaries.
|
|
|
|
#### "Customer cannot access admin/store 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 store 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
|
|
|
|
#### Check Cookie in Browser
|
|
|
|
```javascript
|
|
// In browser console
|
|
document.cookie.split(';').forEach(c => console.log(c.trim()));
|
|
```
|
|
|
|
#### Decode JWT Token
|
|
|
|
```javascript
|
|
// 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
|
|
- Store users should use store portal
|
|
- Customers should use shop
|
|
|
|
3. **Always check user.is_active:**
|
|
```python
|
|
if not current_user.is_active:
|
|
raise UserNotActiveException()
|
|
```
|
|
|
|
4. **Use type hints:**
|
|
```python
|
|
def my_route(current_user: User = Depends(get_current_admin_api)):
|
|
# IDE will have autocomplete for current_user
|
|
```
|
|
|
|
5. **Handle exceptions properly:**
|
|
```python
|
|
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:**
|
|
```javascript
|
|
const token = localStorage.getItem('token');
|
|
fetch('/api/v1/admin/stores', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
```
|
|
|
|
3. **Handle 401/403 responses:**
|
|
```javascript
|
|
if (response.status === 401) {
|
|
// Redirect to login
|
|
window.location.href = '/admin/login';
|
|
}
|
|
```
|
|
|
|
4. **Clear tokens on logout:**
|
|
```javascript
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
```
|
|
|
|
### Cookie Expiration
|
|
|
|
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:
|
|
```python
|
|
# 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_store
|
|
- require_customer
|
|
- create_default_admin_user
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
For a condensed cheat sheet of authentication patterns, see [Authentication Quick Reference](authentication-quick-reference.md).
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [RBAC System](rbac.md) - Role-based access control and permissions
|
|
- [Architecture Overview](../architecture/auth-rbac.md) - System-wide authentication architecture
|
|
- [Backend Development](../backend/overview.md) - Backend development guide
|
|
- [API Reference](../backend/middleware-reference.md) - Auto-generated API documentation
|
|
|
|
---
|
|
|
|
**Document Version:** 1.0
|
|
**Last Updated:** November 2025
|
|
**Maintained By:** Backend Team
|