# Vendor-in-Token Architecture ## Overview This document describes the vendor-in-token authentication architecture used for vendor API endpoints. This architecture embeds vendor context directly into JWT tokens, eliminating the need for URL-based vendor detection and enabling clean, RESTful API endpoints. ## The Problem: URL-Based Vendor Detection ### Old Pattern (Deprecated) ```python # ❌ DEPRECATED: URL-based vendor detection @router.get("/{product_id}") def get_product( product_id: int, vendor: Vendor = Depends(require_vendor_context()), # ❌ Don't use current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): product = product_service.get_product(db, vendor.id, product_id) return product ``` ### Issues with URL-Based Detection 1. **Inconsistent API Routes** - Page routes: `/vendor/{vendor_code}/dashboard` (has vendor in URL) - API routes: `/api/v1/vendor/products` (no vendor in URL) - `require_vendor_context()` only works when vendor is in the URL path 2. **404 Errors on API Endpoints** - API calls to `/api/v1/vendor/products` would return 404 - The dependency expected vendor 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 vendor context - Harder to test and maintain ## The Solution: Vendor-in-Token ### Architecture Overview ``` ┌─────────────────────────────────────────────────────────────────┐ │ Vendor Login Flow │ └─────────────────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 1. Authenticate user credentials │ │ 2. Validate vendor membership │ │ 3. Create JWT with vendor context: │ │ { │ │ "sub": "user_id", │ │ "username": "john.doe", │ │ "vendor_id": 123, ← Vendor context in token │ │ "vendor_code": "WIZAMART", ← Vendor code in token │ │ "vendor_role": "Owner" ← Vendor role in token │ │ } │ └─────────────────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 4. Set dual token storage: │ │ - HTTP-only cookie (path=/vendor) for page navigation │ │ - Response body for localStorage (API calls) │ └─────────────────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 5. Subsequent API requests include vendor context │ │ Authorization: Bearer │ └─────────────────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 6. get_current_vendor_api() extracts vendor from token: │ │ - current_user.token_vendor_id │ │ - current_user.token_vendor_code │ │ - current_user.token_vendor_role │ │ 7. Validates user still has access to vendor │ └─────────────────────────────────────────────────────────────────┘ ``` ### Implementation Components #### 1. Token Creation (middleware/auth.py) ```python def create_access_token( self, user: User, vendor_id: int | None = None, vendor_code: str | None = None, vendor_role: str | None = None, ) -> dict[str, Any]: """Create JWT with optional vendor context.""" payload = { "sub": str(user.id), "username": user.username, "email": user.email, "role": user.role, "exp": expire, "iat": datetime.now(UTC), } # Include vendor information in token if provided if vendor_id is not None: payload["vendor_id"] = vendor_id if vendor_code is not None: payload["vendor_code"] = vendor_code if vendor_role is not None: payload["vendor_role"] = vendor_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. Vendor Login (app/api/v1/vendor/auth.py) ```python @router.post("/login", response_model=VendorLoginResponse) def vendor_login( user_credentials: UserLogin, response: Response, db: Session = Depends(get_db), ): """ Vendor team member login. Creates vendor-scoped JWT token with vendor context embedded. """ # Authenticate user and determine vendor login_result = auth_service.login_user(db=db, user_credentials=user_credentials) user = login_result["user"] # Determine vendor and role vendor = determine_vendor(db, user) # Your vendor detection logic vendor_role = determine_role(db, user, vendor) # Your role detection logic # Create vendor-scoped access token token_data = auth_service.auth_manager.create_access_token( user=user, vendor_id=vendor.id, vendor_code=vendor.vendor_code, vendor_role=vendor_role, ) # Set cookie and return token response.set_cookie( key="vendor_token", value=token_data["access_token"], httponly=True, path="/vendor", # Restricted to vendor routes ) return VendorLoginResponse(**token_data, user=user, vendor=vendor) ``` #### 3. Token Verification (app/api/deps.py) ```python def get_current_vendor_api( authorization: str | None = Header(None, alias="Authorization"), db: Session = Depends(get_db), ) -> User: """ Get current vendor API user from Authorization header. Extracts vendor 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 vendor access if token is vendor-scoped if hasattr(user, "token_vendor_id"): vendor_id = user.token_vendor_id # Verify user still has access to this vendor if not user.is_member_of(vendor_id): raise InsufficientPermissionsException( "Access to vendor has been revoked. Please login again." ) return user ``` #### 4. Endpoint Usage (app/api/v1/vendor/products.py) ```python @router.get("", response_model=ProductListResponse) def get_vendor_products( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), current_user: User = Depends(get_current_vendor_api), # ✅ Only need this db: Session = Depends(get_db), ): """ Get all products in vendor catalog. Vendor is determined from JWT token (vendor_id claim). """ # Extract vendor ID from token if not hasattr(current_user, "token_vendor_id"): raise HTTPException( status_code=400, detail="Token missing vendor information. Please login again.", ) vendor_id = current_user.token_vendor_id # Use vendor_id from token for business logic products, total = product_service.get_vendor_products( db=db, vendor_id=vendor_id, skip=skip, limit=limit, ) return ProductListResponse(products=products, total=total) ``` ## Migration Guide ### Step 1: Identify Endpoints Using require_vendor_context() Search for all occurrences: ```bash grep -r "require_vendor_context" app/api/v1/vendor/ ``` ### Step 2: Update Endpoint Signature **Before:** ```python @router.get("/{product_id}") def get_product( product_id: int, vendor: Vendor = Depends(require_vendor_context()), # ❌ Remove this current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): ``` **After:** ```python @router.get("/{product_id}") def get_product( product_id: int, current_user: User = Depends(get_current_vendor_api), # ✅ Only need this db: Session = Depends(get_db), ): ``` ### Step 3: Extract Vendor from Token **Before:** ```python product = product_service.get_product(db, vendor.id, product_id) ``` **After:** ```python from fastapi import HTTPException # Extract vendor ID from token if not hasattr(current_user, "token_vendor_id"): raise HTTPException( status_code=400, detail="Token missing vendor information. Please login again.", ) vendor_id = current_user.token_vendor_id # Use vendor_id from token product = product_service.get_product(db, vendor_id, product_id) ``` ### Step 4: Update Logging References **Before:** ```python logger.info(f"Product updated for vendor {vendor.vendor_code}") ``` **After:** ```python logger.info(f"Product updated for vendor {current_user.token_vendor_code}") ``` ### Complete Migration Example **Before (URL-based vendor detection):** ```python @router.put("/{product_id}", response_model=ProductResponse) def update_product( product_id: int, product_data: ProductUpdate, vendor: Vendor = Depends(require_vendor_context()), # ❌ current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Update product in vendor catalog.""" product = product_service.update_product( db=db, vendor_id=vendor.id, # ❌ From URL product_id=product_id, product_update=product_data ) logger.info( f"Product {product_id} updated by {current_user.username} " f"for vendor {vendor.vendor_code}" # ❌ From URL ) return ProductResponse.model_validate(product) ``` **After (Token-based vendor context):** ```python @router.put("/{product_id}", response_model=ProductResponse) def update_product( product_id: int, product_data: ProductUpdate, current_user: User = Depends(get_current_vendor_api), # ✅ Only dependency db: Session = Depends(get_db), ): """Update product in vendor catalog.""" from fastapi import HTTPException # Extract vendor ID from token if not hasattr(current_user, "token_vendor_id"): raise HTTPException( status_code=400, detail="Token missing vendor information. Please login again.", ) vendor_id = current_user.token_vendor_id # ✅ From token product = product_service.update_product( db=db, vendor_id=vendor_id, # ✅ From token product_id=product_id, product_update=product_data ) logger.info( f"Product {product_id} updated by {current_user.username} " f"for vendor {current_user.token_vendor_code}" # ✅ From token ) return ProductResponse.model_validate(product) ``` ## Files to Migrate Current files still using `require_vendor_context()`: - `app/api/v1/vendor/customers.py` - `app/api/v1/vendor/notifications.py` - `app/api/v1/vendor/media.py` - `app/api/v1/vendor/marketplace.py` - `app/api/v1/vendor/inventory.py` - `app/api/v1/vendor/settings.py` - `app/api/v1/vendor/analytics.py` - `app/api/v1/vendor/payments.py` - `app/api/v1/vendor/profile.py` ## Benefits of Vendor-in-Token ### 1. Clean RESTful APIs ``` ✅ /api/v1/vendor/products ✅ /api/v1/vendor/orders ✅ /api/v1/vendor/customers ❌ /api/v1/vendor/{vendor_code}/products (unnecessary vendor in URL) ``` ### 2. Security - Vendor 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 vendor API endpoints - No confusion between page routes and API routes - Single source of truth (the token) ### 4. Performance - No database lookup for vendor context on every request - Vendor 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 vendor 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 vendor (database check) ### Access Revocation If a user's vendor access is revoked: 1. Existing tokens remain valid until expiration 2. `get_current_vendor_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 vendor membership ## Testing ### Unit Tests ```python def test_vendor_in_token(): """Test vendor context in JWT token.""" # Create token with vendor context token_data = auth_manager.create_access_token( user=user, vendor_id=123, vendor_code="WIZAMART", vendor_role="Owner", ) # Verify token contains vendor data payload = jwt.decode(token_data["access_token"], secret_key) assert payload["vendor_id"] == 123 assert payload["vendor_code"] == "WIZAMART" assert payload["vendor_role"] == "Owner" def test_api_endpoint_uses_token_vendor(): """Test API endpoint extracts vendor from token.""" response = client.get( "/api/v1/vendor/products", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 # Verify products are filtered by token vendor_id ``` ### Integration Tests ```python def test_vendor_login_and_api_access(): """Test full vendor login and API access flow.""" # Login as vendor user response = client.post("/api/v1/vendor/auth/login", json={ "username": "john.doe", "password": "password123" }) assert response.status_code == 200 token = response.json()["access_token"] # Access vendor API with token response = client.get( "/api/v1/vendor/products", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 # Verify vendor context from token products = response.json()["products"] # All products should belong to token vendor ``` ## Architecture Rules See `docs/architecture/rules/API-VND-001.md` for the formal architecture rule enforcing this pattern. ## Related Documentation - [Vendor RBAC System](./vendor-rbac.md) - Role-based access control for vendors - [Vendor Authentication](./vendor-authentication.md) - Complete authentication guide - [Architecture Rules](../architecture/rules/) - All architecture rules - [API Design Guidelines](../architecture/api-design.md) - RESTful API patterns ## Summary The vendor-in-token architecture: - ✅ Embeds vendor context in JWT tokens - ✅ Eliminates URL-based vendor detection - ✅ Enables clean RESTful API endpoints - ✅ Improves security and performance - ✅ Simplifies endpoint implementation - ✅ Follows architecture best practices **Migration Status:** In progress - 9 endpoint files remaining to migrate