- Add Development URL Quick Reference section to url-routing overview with all login URLs, entry points, and full examples - Replace /shop/ path segments with /storefront/ across 50 docs files - Update file references: shop_pages.py → storefront_pages.py, templates/shop/ → templates/storefront/, api/v1/shop/ → api/v1/storefront/ - Preserve domain references (orion.shop) and /store/ staff dashboard paths - Archive docs left unchanged (historical) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
23 KiB
Store-in-Token Architecture
Overview
This document describes the store-in-token authentication architecture used for store API endpoints. This architecture embeds store context directly into JWT tokens, eliminating the need for URL-based store detection and enabling clean, RESTful API endpoints.
The Problem: URL-Based Store Detection
Old Pattern (Deprecated)
# ❌ DEPRECATED: URL-based store detection
@router.get("/{product_id}")
def get_product(
product_id: int,
store: Store = Depends(require_store_context()), # ❌ Don't use
current_user: User = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
product = product_service.get_product(db, store.id, product_id)
return product
Issues with URL-Based Detection
-
Inconsistent API Routes
- Page routes:
/store/{store_code}/dashboard(has store in URL) - API routes:
/api/v1/store/products(no store in URL) require_store_context()only works when store is in the URL path
- Page routes:
-
404 Errors on API Endpoints
- API calls to
/api/v1/store/productswould return 404 - The dependency expected store code in URL but API routes don't have it
- Breaking RESTful API design principles
- API calls to
-
Architecture Violation
- Mixed concerns: URL structure determining business logic
- Tight coupling between routing and store context
- Harder to test and maintain
The Solution: Store-in-Token
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Store Login Flow │
└─────────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────────┐
│ 1. Authenticate user credentials │
│ 2. Validate store membership │
│ 3. Create JWT with store context: │
│ { │
│ "sub": "user_id", │
│ "username": "john.doe", │
│ "store_id": 123, ← Store context in token │
│ "store_code": "ORION", ← Store code in token │
│ "store_role": "Owner" ← Store role in token │
│ } │
└─────────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. Set dual token storage: │
│ - HTTP-only cookie (path=/store) for page navigation │
│ - Response body for localStorage (API calls) │
└─────────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. Subsequent API requests include store context │
│ Authorization: Bearer <token-with-store-context> │
└─────────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────────┐
│ 6. get_current_store_api() extracts store from token: │
│ - current_user.token_store_id │
│ - current_user.token_store_code │
│ - current_user.token_store_role │
│ 7. Validates user still has access to store │
└─────────────────────────────────────────────────────────────────┘
Implementation Components
1. Token Creation (middleware/auth.py)
def create_access_token(
self,
user: User,
store_id: int | None = None,
store_code: str | None = None,
store_role: str | None = None,
) -> dict[str, Any]:
"""Create JWT with optional store context."""
payload = {
"sub": str(user.id),
"username": user.username,
"email": user.email,
"role": user.role,
"exp": expire,
"iat": datetime.now(UTC),
}
# Include store information in token if provided
if store_id is not None:
payload["store_id"] = store_id
if store_code is not None:
payload["store_code"] = store_code
if store_role is not None:
payload["store_role"] = store_role
return {
"access_token": jwt.encode(payload, self.secret_key, algorithm=self.algorithm),
"token_type": "bearer",
"expires_in": self.access_token_expire_minutes * 60,
}
2. Store Login (app/api/v1/store/auth.py)
@router.post("/login", response_model=StoreLoginResponse)
def store_login(
user_credentials: UserLogin,
response: Response,
db: Session = Depends(get_db),
):
"""
Store team member login.
Creates store-scoped JWT token with store context embedded.
"""
# Authenticate user and determine store
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
user = login_result["user"]
# Determine store and role
store = determine_store(db, user) # Your store detection logic
store_role = determine_role(db, user, store) # Your role detection logic
# Create store-scoped access token
token_data = auth_service.auth_manager.create_access_token(
user=user,
store_id=store.id,
store_code=store.store_code,
store_role=store_role,
)
# Set cookie and return token
response.set_cookie(
key="store_token",
value=token_data["access_token"],
httponly=True,
path="/store", # Restricted to store routes
)
return StoreLoginResponse(**token_data, user=user, store=store)
3. Token Verification (app/api/deps.py)
def get_current_store_api(
authorization: str | None = Header(None, alias="Authorization"),
db: Session = Depends(get_db),
) -> User:
"""
Get current store API user from Authorization header.
Extracts store context from JWT token and validates access.
"""
if not authorization or not authorization.startswith("Bearer "):
raise AuthenticationException("Authorization header required for API calls")
token = authorization.replace("Bearer ", "")
user = auth_service.auth_manager.get_current_user(token, db)
# Validate store access if token is store-scoped
if hasattr(user, "token_store_id"):
store_id = user.token_store_id
# Verify user still has access to this store
if not user.is_member_of(store_id):
raise InsufficientPermissionsException(
"Access to store has been revoked. Please login again."
)
return user
4. Endpoint Usage (app/api/v1/store/products.py)
@router.get("", response_model=ProductListResponse)
def get_store_products(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
current_user: User = Depends(get_current_store_api), # ✅ Guarantees token_store_id
db: Session = Depends(get_db),
):
"""
Get all products in store catalog.
Store is determined from JWT token (store_id claim).
The get_current_store_api dependency GUARANTEES token_store_id is present.
"""
# Use store_id from token for business logic
# NO validation needed - dependency guarantees token_store_id exists
products, total = product_service.get_store_products(
db=db,
store_id=current_user.token_store_id, # Safe to use directly
skip=skip,
limit=limit,
)
return ProductListResponse(products=products, total=total)
Important
: The
get_current_store_api()dependency now guarantees thattoken_store_idis present. Endpoints should NOT check for its existence - this would be redundant validation that belongs in the dependency layer.
Migration Guide
Step 1: Identify Endpoints Using require_store_context()
Search for all occurrences:
grep -r "require_store_context" app/api/v1/store/
Step 2: Update Endpoint Signature
Before:
@router.get("/{product_id}")
def get_product(
product_id: int,
store: Store = Depends(require_store_context()), # ❌ Remove this
current_user: User = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
After:
@router.get("/{product_id}")
def get_product(
product_id: int,
current_user: User = Depends(get_current_store_api), # ✅ Only need this
db: Session = Depends(get_db),
):
Step 3: Extract Store from Token
Before:
product = product_service.get_product(db, store.id, product_id)
After:
# Use store_id from token directly - dependency guarantees it exists
product = product_service.get_product(db, current_user.token_store_id, product_id)
Note
: Do NOT add validation like
if not hasattr(current_user, "token_store_id"). Theget_current_store_apidependency guarantees this attribute is present. Adding such checks violates the architecture rule API-003 (endpoints should not raise exceptions).
Step 4: Update Logging References
Before:
logger.info(f"Product updated for store {store.store_code}")
After:
logger.info(f"Product updated for store {current_user.token_store_code}")
Complete Migration Example
Before (URL-based store detection):
@router.put("/{product_id}", response_model=ProductResponse)
def update_product(
product_id: int,
product_data: ProductUpdate,
store: Store = Depends(require_store_context()), # ❌
current_user: User = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
"""Update product in store catalog."""
product = product_service.update_product(
db=db,
store_id=store.id, # ❌ From URL
product_id=product_id,
product_update=product_data
)
logger.info(
f"Product {product_id} updated by {current_user.username} "
f"for store {store.store_code}" # ❌ From URL
)
return ProductResponse.model_validate(product)
After (Token-based store context):
@router.put("/{product_id}", response_model=ProductResponse)
def update_product(
product_id: int,
product_data: ProductUpdate,
current_user: User = Depends(get_current_store_api), # ✅ Guarantees token_store_id
db: Session = Depends(get_db),
):
"""Update product in store catalog."""
# NO validation needed - dependency guarantees token_store_id exists
product = product_service.update_product(
db=db,
store_id=current_user.token_store_id, # ✅ From token - safe to use directly
product_id=product_id,
product_update=product_data
)
logger.info(
f"Product {product_id} updated by {current_user.username} "
f"for store {current_user.token_store_code}" # ✅ From token
)
return ProductResponse.model_validate(product)
Architecture Rule API-003: Endpoints should NOT raise exceptions. The
get_current_store_apidependency handles all validation and raisesInvalidTokenExceptioniftoken_store_idis missing.
Migration Status
COMPLETED - All store API endpoints have been migrated to use the token-based store context pattern.
Migrated Files
All store API files now use current_user.token_store_id:
app/api/v1/store/customers.py✅app/api/v1/store/notifications.py✅app/api/v1/store/media.py✅app/api/v1/store/marketplace.py✅app/api/v1/store/inventory.py✅app/api/v1/store/settings.py✅app/api/v1/store/analytics.py✅app/api/v1/store/payments.py✅app/api/v1/store/profile.py✅app/api/v1/store/dashboard.py✅app/api/v1/store/products.py✅app/api/v1/store/orders.py✅app/api/v1/store/team.py✅ (uses permission dependencies)
Permission Dependencies Updated
The following permission dependencies now use token-based store context:
require_store_permission()- Gets store from token, setsrequest.state.storerequire_store_owner- Gets store from token, setsrequest.state.storerequire_any_store_permission()- Gets store from token, setsrequest.state.storerequire_all_store_permissions()- Gets store from token, setsrequest.state.storeget_user_permissions- Gets store from token, setsrequest.state.store
Storefront Endpoints
Storefront endpoints (public, no authentication) still use require_store_context():
app/api/v1/storefront/products.py- Uses URL/subdomain/domain detectionapp/api/v1/storefront/cart.py- Uses URL/subdomain/domain detection
This is correct behavior - storefront endpoints need to detect store from the request URL, not from JWT token.
Benefits of Store-in-Token
1. Clean RESTful APIs
✅ /api/v1/store/products
✅ /api/v1/store/orders
✅ /api/v1/store/customers
❌ /api/v1/store/{store_code}/products (unnecessary store in URL)
2. Security
- Store context cryptographically signed in JWT
- Cannot be tampered with by client
- Automatic validation on every request
- Token revocation possible via database checks
3. Consistency
- Same authentication mechanism for all store API endpoints
- No confusion between page routes and API routes
- Single source of truth (the token)
4. Performance
- No database lookup for store context on every request
- Store information already in token payload
- Optional validation for revoked access
5. Maintainability
- Simpler endpoint signatures
- Less boilerplate code
- Easier to test
- Follows architecture rule API-002 (no DB queries in endpoints)
Security Considerations
Token Validation
The token store context is validated on every request:
- JWT signature verification (ensures token not tampered with)
- Token expiration check (typically 30 minutes)
- Optional: Verify user still member of store (database check)
Access Revocation
If a user's store access is revoked:
- Existing tokens remain valid until expiration
get_current_store_api()performs optional database check- User forced to re-login after token expires
- New login will fail if access revoked
Token Refresh
Tokens should be refreshed periodically:
- Default: 30 minutes expiration
- Refresh before expiration for seamless UX
- New login creates new token with current store membership
Testing
Unit Tests
def test_store_in_token():
"""Test store context in JWT token."""
# Create token with store context
token_data = auth_manager.create_access_token(
user=user,
store_id=123,
store_code="ORION",
store_role="Owner",
)
# Verify token contains store data
payload = jwt.decode(token_data["access_token"], secret_key)
assert payload["store_id"] == 123
assert payload["store_code"] == "ORION"
assert payload["store_role"] == "Owner"
def test_api_endpoint_uses_token_store():
"""Test API endpoint extracts store from token."""
response = client.get(
"/api/v1/store/products",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
# Verify products are filtered by token store_id
Integration Tests
def test_store_login_and_api_access():
"""Test full store login and API access flow."""
# Login as store user
response = client.post("/api/v1/store/auth/login", json={
"username": "john.doe",
"password": "password123"
})
assert response.status_code == 200
token = response.json()["access_token"]
# Access store API with token
response = client.get(
"/api/v1/store/products",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
# Verify store context from token
products = response.json()["products"]
# All products should belong to token store
Architecture Rules and Design Pattern Enforcement
The Layered Exception Pattern
The architecture enforces a strict layered pattern for where exceptions should be raised:
┌────────────────────────────────────────────────────────────────────────────┐
│ ENDPOINTS (Thin Layer) - app/api/v1/**/*.py │
│ │
│ ❌ MUST NOT raise exceptions │
│ ❌ MUST NOT check hasattr(current_user, 'token_store_id') │
│ ✅ MUST trust dependencies to handle validation │
│ ✅ MUST directly use current_user.token_store_id │
└────────────────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ DEPENDENCIES (Validation Layer) - app/api/deps.py │
│ │
│ ✅ MUST raise InvalidTokenException if token_store_id missing │
│ ✅ MUST validate user still has store access │
│ ✅ GUARANTEES token_store_id, token_store_code, token_store_role │
└────────────────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ SERVICES (Business Logic) - app/services/**/*.py │
│ │
│ ✅ MUST raise domain exceptions for business rule violations │
│ ✅ Examples: StoreNotFoundException, ProductNotFoundException │
└────────────────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ GLOBAL EXCEPTION HANDLER - app/exceptions/handler.py │
│ │
│ ✅ Catches all OrionException subclasses │
│ ✅ Converts to appropriate HTTP responses │
│ ✅ Provides consistent error formatting │
└────────────────────────────────────────────────────────────────────────────┘
Enforced by Architecture Validation
The validation script (scripts/validate/validate_architecture.py) enforces these rules:
Rule API-003: Endpoints must NOT raise exceptions directly
- Detects
raise HTTPException,raise InvalidTokenException, etc. in endpoint files - Detects redundant validation like
if not hasattr(current_user, 'token_store_id') - Blocks commits via pre-commit hook if violations found
Pre-commit Hook
Architecture validation runs on every commit:
# .pre-commit-config.yaml
- repo: local
hooks:
- id: validate-architecture
name: Validate Architecture Patterns
entry: python scripts/validate/validate_architecture.py
language: python
pass_filenames: false
always_run: true
To run manually:
python scripts/validate/validate_architecture.py # Full validation
python scripts/validate/validate_architecture.py -d app/api/v1/store/ # Specific directory
See .architecture-rules.yaml for the complete rule definitions.
Related Documentation
- Store RBAC System - Role-based access control for stores
- Authentication & RBAC - Complete authentication guide
- Architecture Patterns - All architecture patterns
- Middleware Reference - Middleware patterns
Summary
The store-in-token architecture:
- ✅ Embeds store context in JWT tokens
- ✅ Eliminates URL-based store detection
- ✅ Enables clean RESTful API endpoints
- ✅ Improves security and performance
- ✅ Simplifies endpoint implementation
- ✅ Follows architecture best practices
Migration Status: ✅ COMPLETED - All store API endpoints migrated and architecture rules enforced