Files
orion/docs/backend/store-in-token-architecture.md
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
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>
2026-02-14 16:46:56 +01:00

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

  1. 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
  2. 404 Errors on API Endpoints

    • API calls to /api/v1/store/products would return 404
    • The dependency expected store code in URL but API routes don't have it
    • Breaking RESTful API design principles
  3. 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 that token_store_id is 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"). The get_current_store_api dependency 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_api dependency handles all validation and raises InvalidTokenException if token_store_id is 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, sets request.state.store
  • require_store_owner - Gets store from token, sets request.state.store
  • require_any_store_permission() - Gets store from token, sets request.state.store
  • require_all_store_permissions() - Gets store from token, sets request.state.store
  • get_user_permissions - Gets store from token, sets request.state.store

Shop Endpoints

Shop endpoints (public, no authentication) still use require_store_context():

  • app/api/v1/shop/products.py - Uses URL/subdomain/domain detection
  • app/api/v1/shop/cart.py - Uses URL/subdomain/domain detection

This is correct behavior - shop 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:

  1. JWT signature verification (ensures token not tampered with)
  2. Token expiration check (typically 30 minutes)
  3. Optional: Verify user still member of store (database check)

Access Revocation

If a user's store access is revoked:

  1. Existing tokens remain valid until expiration
  2. get_current_store_api() performs optional database check
  3. User forced to re-login after token expires
  4. 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.

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