# Customer Authentication Implementation **Date**: 2025-11-24 **Status**: Completed ## Overview This document describes the implementation of customer authentication for the storefront frontend, including login, registration, and account management pages. This work creates a complete separation between customer authentication and admin/store authentication systems. ## Problem Statement The storefront frontend needed proper authentication pages (login, registration, forgot password) and a working customer authentication system. The initial implementation had several issues: 1. No styled authentication pages for customers 2. Customer authentication was incorrectly trying to use the User model (admins/stores) 3. Cookie paths were hardcoded and didn't work with multi-access routing (domain, subdomain, path-based) 4. Store detection method was inconsistent between direct path access and API calls via referer ## Solution Architecture ### 1. Customer vs User Separation **Key Insight**: Customers are NOT users. They are a separate entity in the system. - **Users** (`models/database/user.py`): Admin and store accounts - Have `role` field (admin/store) - Have `username` field - Managed via `app/services/auth_service.py` - **Customers** (`models/database/customer.py`): Storefront customers - Store-scoped (each store has independent customers) - No `role` or `username` fields - Have `customer_number`, `total_orders`, store relationship - Managed via `app/services/customer_service.py` ### 2. JWT Token Structure Customer tokens have a distinct structure: ```python { "sub": str(customer.id), # Customer ID "email": customer.email, "store_id": store_id, # Important: Store isolation "type": "customer", # CRITICAL: Distinguishes from User tokens "exp": expire_timestamp, "iat": issued_at_timestamp, } ``` User tokens have `type` implicitly set to user role (admin/store) and different payload structure. ### 3. Cookie Path Management Cookies must be set with paths that match how the store is accessed: | Access Method | Example URL | Cookie Path | |--------------|-------------|-------------| | Domain | `orion.lu/storefront/account/login` | `/storefront` | | Subdomain | `orion.localhost/storefront/account/login` | `/storefront` | | Path-based | `localhost/stores/orion/storefront/account/login` | `/storefront/orion` | This ensures cookies are only sent to the correct store's routes. ## Implementation Details ### Files Created 1. **`app/templates/storefront/account/login.html`** - Customer login page - Extends `storefront/base.html` (follows design pattern) - Uses Tailwind CSS and Alpine.js - Theme-aware styling with CSS variables - Two-column layout (branding + form) - Form validation and error handling 2. **`app/templates/storefront/account/register.html`** - Customer registration page - Fields: first_name, last_name, email, phone (optional), password - Client-side validation - Marketing consent checkbox - Theme integration 3. **`app/templates/storefront/account/forgot-password.html`** - Password reset request page - Two-state UI (form → success) - Email validation 4. **`app/templates/storefront/account/dashboard.html`** - Customer account dashboard - Displays account summary, order statistics - Quick links to orders, profile, addresses - Logout functionality - Follows storefront design pattern (extends base.html) ### Important Schema Change #### `models/schema/auth.py` - Unified Login Schema Changed the `UserLogin` schema to use `email_or_username` instead of `username` to support both username and email login across all contexts (admin, store, and customer). **Before**: ```python class UserLogin(BaseModel): username: str password: str ``` **After**: ```python class UserLogin(BaseModel): email_or_username: str = Field(..., description="Username or email address") password: str store_code: Optional[str] = Field(None, description="Optional store code for context") ``` **Impact**: This change affects all login endpoints: - Admin login: `/api/v1/admin/auth/login` - Store login: `/api/v1/store/auth/login` - Customer login: `/api/v1/storefront/auth/login` **Updated Files**: - `app/services/auth_service.py` - Changed `user_credentials.username` to `user_credentials.email_or_username` - `app/api/v1/admin/auth.py` - Updated logging to use `email_or_username` - `static/admin/js/login.js` - Send `email_or_username` in payload - `static/store/js/login.js` - Send `email_or_username` in payload ### Files Modified #### 1. `app/api/v1/storefront/auth.py` **Changes**: - Added `CustomerLoginResponse` model (uses `CustomerResponse` instead of `UserResponse`) - Updated `customer_login` endpoint to: - Calculate cookie path dynamically based on store access method - Set cookie with correct path for multi-access support - Return `CustomerLoginResponse` with proper customer data - Updated `customer_logout` endpoint to calculate cookie path dynamically **Key Code**: ```python # Calculate cookie path based on store access method store_context = getattr(request.state, 'store_context', None) access_method = store_context.get('detection_method', 'unknown') if store_context else 'unknown' cookie_path = "/storefront" # Default for domain/subdomain access if access_method == "path": # For path-based access like /storefront/orion full_prefix = store_context.get('full_prefix', '/store/') if store_context else '/store/' cookie_path = f"/storefront/{store.subdomain}" response.set_cookie( key="customer_token", value=token, httponly=True, secure=should_use_secure_cookies(), samesite="lax", max_age=expires_in, path=cookie_path, # Dynamic path ) ``` #### 2. `app/services/customer_service.py` **Changes**: - Updated `login_customer` to create JWT tokens directly using `auth_manager` - No longer tries to use `auth_service.create_access_token()` (that's for Users only) - Directly uses `jose.jwt.encode()` with custom customer payload **Key Code**: ```python from jose import jwt from datetime import datetime, timedelta, timezone auth_manager = self.auth_service.auth_manager expires_delta = timedelta(minutes=auth_manager.token_expire_minutes) expire = datetime.now(timezone.utc) + expires_delta payload = { "sub": str(customer.id), "email": customer.email, "store_id": store_id, "type": "customer", # Critical distinction "exp": expire, "iat": datetime.now(timezone.utc), } token = jwt.encode(payload, auth_manager.secret_key, algorithm=auth_manager.algorithm) ``` #### 3. `app/api/deps.py` **Major Rewrite**: `get_current_customer_from_cookie_or_header` **Before**: Tried to validate customer tokens as User tokens, expected `role` field **After**: - Decodes JWT manually - Validates `type == "customer"` in payload - Loads Customer from database (not User) - Returns Customer object **Key Code**: ```python def get_current_customer_from_cookie_or_header( request: Request, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), customer_token: Optional[str] = Cookie(None), db: Session = Depends(get_db), ): from models.database.customer import Customer from jose import jwt, JWTError token, source = _get_token_from_request(...) if not token: raise InvalidTokenException("Customer authentication required") # Decode and validate customer JWT token payload = jwt.decode(token, auth_manager.secret_key, algorithms=[auth_manager.algorithm]) # Verify this is a customer token if payload.get("type") != "customer": raise InvalidTokenException("Customer authentication required") customer_id = payload.get("sub") customer = db.query(Customer).filter(Customer.id == int(customer_id)).first() if not customer or not customer.is_active: raise InvalidTokenException("Customer not found or inactive") return customer # Returns Customer, not User ``` #### 4. `app/routes/storefront_pages.py` **Changes**: - Changed import from `User` to `Customer` - Updated all protected route handlers: - Changed parameter type from `current_user: User` to `current_customer: Customer` - Updated function calls from `user=current_user` to `user=current_customer` **Affected Routes**: - `/account/dashboard` - `/account/orders` - `/account/orders/{order_id}` - `/account/profile` - `/account/addresses` - `/account/wishlist` - `/account/reviews` #### 5. `middleware/store_context.py` **Critical Fix**: Harmonized store detection methods **Problem**: - Direct page access: `detection_method = "path"` - API call via referer: `detection_method = "referer_path"` - This inconsistency broke cookie path calculation **Solution**: When detecting store from referer path, use the same `detection_method = "path"` and include the same fields (`full_prefix`, `path_prefix`) as direct path detection. **Key Code**: ```python # Method 1: Path-based detection from referer path if referer_path.startswith("/stores/") or referer_path.startswith("/store/"): prefix = "/stores/" if referer_path.startswith("/stores/") else "/store/" path_parts = referer_path[len(prefix):].split("/") if len(path_parts) >= 1 and path_parts[0]: store_code = path_parts[0] prefix_len = len(prefix) # Use "path" as detection_method to be consistent with direct path detection return { "subdomain": store_code, "detection_method": "path", # Consistent! "path_prefix": referer_path[:prefix_len + len(store_code)], "full_prefix": prefix, "host": referer_host, "referer": referer, } ``` #### 6. `app/exceptions/handler.py` **Changes**: - Password sanitization in validation error logging - Already had proper redirect logic for customer login (no changes needed) #### 7. `models/schema/auth.py` **Changes**: - Updated `UserLogin` schema to accept `email_or_username` instead of `username` - This allows customers to login with email (they don't have usernames) ### Multi-Access Routing Support The implementation properly supports all three store access methods: #### Domain-based Access ``` URL: https://orion.lu/storefront/account/login Cookie Path: /storefront Cookie Sent To: https://orion.lu/storefront/* ``` #### Subdomain-based Access ``` URL: https://orion.myplatform.com/storefront/account/login Cookie Path: /storefront Cookie Sent To: https://orion.myplatform.com/storefront/* ``` #### Path-based Access ``` URL: https://myplatform.com/storefront/orion/account/login Cookie Path: /storefront/orion Cookie Sent To: https://myplatform.com/storefront/orion/* ``` ## Authentication Flow ### Login Flow 1. **User loads login page** → `GET /storefront/orion/account/login` - Middleware detects store from path - Sets `detection_method = "path"` in store_context - Renders login template 2. **User submits credentials** → `POST /api/v1/storefront/auth/login` - Middleware detects store from Referer header - Sets `detection_method = "path"` (harmonized!) - Validates credentials via `customer_service.login_customer()` - Creates JWT token with `type: "customer"` - Calculates cookie path based on access method - Sets `customer_token` cookie with correct path - Returns token + customer data 3. **Browser redirects to dashboard** → `GET /storefront/orion/account/dashboard` - Browser sends `customer_token` cookie (path matches!) - Dependency `get_current_customer_from_cookie_or_header` extracts token - Decodes JWT, validates `type == "customer"` - Loads Customer from database - Renders dashboard with customer data ### Logout Flow 1. **User clicks logout button** → Shows Tailwind modal confirmation - Custom modal (not browser confirm dialog) - Alpine.js state management - Smooth animations with transitions - Dark mode support 2. **User confirms logout** → `POST /api/v1/storefront/auth/logout` - Calculates cookie path (same logic as login) - Deletes cookie with matching path - Returns success message 3. **Frontend redirects to login page** - Shows success toast notification - Clears localStorage token - Redirects after 500ms delay ## Security Features ### Cookie Security ```python response.set_cookie( key="customer_token", value=token, httponly=True, # JavaScript cannot access (XSS protection) secure=True, # HTTPS only (production/staging) samesite="lax", # CSRF protection max_age=1800, # 30 minutes (matches JWT expiry) path=cookie_path, # Restricted to store's storefront routes ) ``` ### Token Validation - JWT expiration checked - Customer active status verified - Token type validated (`type == "customer"`) - Store isolation enforced (customer must belong to store) ### Password Security - Bcrypt hashing via `auth_manager.hash_password()` - Validation errors sanitized (passwords never logged) - Minimum password length enforced ## Design Patterns Followed ### Frontend Templates All authentication pages follow the storefront template pattern: ```jinja2 {% extends "storefront/base.html" %} {% block title %}Page Title{% endblock %} {% block alpine_data %}componentName(){% endblock %} {% block content %} {% endblock %} {% block extra_scripts %} {% endblock %} ``` **Benefits**: - Consistent header/footer/navigation - Theme CSS variables automatically injected - Dark mode support - Mobile responsive - Alpine.js component pattern ### Service Layer - Customer operations in `customer_service.py` - Auth operations in `auth_service.py` - Clear separation of concerns - Database operations via SQLAlchemy ORM ### Exception Handling - Custom exceptions for customer-specific errors - Consistent error responses (JSON for API, HTML for pages) - Automatic redirect to login on 401 for HTML page requests ### UI Components #### Logout Confirmation Modal Custom Tailwind CSS modal for logout confirmation instead of browser's native `confirm()` dialog. **Features**: - Beautiful animated modal with backdrop overlay - Warning icon (red triangle with exclamation mark) - Clear confirmation message - Two action buttons: "Logout" (red) and "Cancel" (gray) - Dark mode support - Mobile responsive - Keyboard accessible (ARIA attributes) - Click backdrop to dismiss **Implementation**: ```html
``` **Alpine.js Component**: ```javascript function accountDashboard() { return { ...storefrontLayoutData(), showLogoutModal: false, // Modal state confirmLogout() { this.showLogoutModal = false; // Perform logout API call // Show toast notification // Redirect to login } } } ``` **Why Custom Modal vs Browser Confirm**: - ✅ Consistent with design system - ✅ Customizable styling and animations - ✅ Dark mode support - ✅ Better mobile experience - ✅ More professional appearance - ✅ Accessible (ARIA labels, keyboard navigation) - ❌ Browser confirm: Cannot be styled, looks dated, poor mobile UX ## Testing Checklist - [x] Customer can register new account - [x] Customer can login with email/password - [x] Admin can login with username (using unified schema) - [x] Store can login with username (using unified schema) - [x] Cookie is set with correct path for path-based access - [x] Cookie is sent on subsequent requests to dashboard - [x] Customer authentication dependency validates token correctly - [x] Dashboard loads with customer data - [x] Customer can logout - [x] Logout confirmation modal displays correctly - [x] Modal has smooth animations and transitions - [x] Modal supports dark mode - [x] Toast notification shows on logout - [x] Cookie is properly deleted on logout - [x] Unauthorized access redirects to login - [x] Theme styling is applied correctly - [x] Dark mode works - [x] Mobile responsive layout - [x] Admin/store login button spinner aligns correctly ## Known Limitations 1. **Password Reset Not Implemented**: `forgot-password` and `reset-password` endpoints are placeholders (TODO comments in code) 2. **Email Verification Not Implemented**: Customers are immediately active after registration 3. **Session Management**: No refresh tokens, single JWT with 30-minute expiry 4. **Account Pages Are Placeholders**: - `/storefront/account/orders` - needs order history implementation - `/storefront/account/profile` - needs profile editing implementation - `/storefront/account/addresses` - needs address management implementation ## Future Enhancements 1. **Password Reset Flow**: - Generate secure reset tokens - Send password reset emails - Token expiry and validation - Password reset form 2. **Email Verification**: - Send verification email on registration - Verification token validation - Resend verification email 3. **Account Management**: - Edit profile (name, email, phone) - Change password - Manage addresses (CRUD) - View order history with filtering/search - Order tracking 4. **Security Enhancements**: - Refresh tokens for longer sessions - Rate limiting on login/registration - Account lockout after failed attempts - 2FA/MFA support 5. **User Experience**: - Remember me functionality - Social login (OAuth) - Progressive disclosure of forms - Better error messages ## References - **Customer Model**: `models/database/customer.py` - **Customer Service**: `app/services/customer_service.py` - **Auth Endpoints**: `app/api/v1/storefront/auth.py` - **Auth Dependencies**: `app/api/deps.py` - **Storefront Routes**: `app/routes/storefront_pages.py` - **Store Context**: `middleware/store_context.py` - **Templates**: `app/templates/storefront/account/` ## Deployment Notes ### Environment Variables No new environment variables required. Uses existing: - `JWT_SECRET_KEY` - for token signing - `JWT_EXPIRE_MINUTES` - token expiry (default: 30) - `ENVIRONMENT` - for secure cookie setting ### Database No migrations required. Uses existing `customer` table. ### Static Files Ensure these files exist: - `static/shared/js/log-config.js` - `static/shared/js/icons.js` - `static/storefront/js/storefront-layout.js` - `static/shared/js/utils.js` - `static/shared/js/api-client.js` - `static/storefront/css/storefront.css` ## Troubleshooting ### Issue: "No token for path" **Cause**: Cookie path doesn't match request path **Solution**: - Check store context middleware is running - Verify `detection_method` is set correctly - Confirm cookie path calculation includes store subdomain for path-based access ### Issue: "Invalid token type" **Cause**: Trying to use User token for customer route or vice versa **Solution**: - Ensure customer login creates token with `type: "customer"` - Verify dependency checks `type == "customer"` ### Issue: Cookie not sent by browser **Cause**: Cookie path doesn't match or cookie expired **Solution**: - Check browser DevTools → Application → Cookies - Verify cookie path matches request URL - Check cookie expiry timestamp ## Summary This implementation establishes a complete customer authentication system that is: ✅ **Secure**: HTTP-only cookies, CSRF protection, password hashing ✅ **Scalable**: Multi-tenant with store isolation ✅ **Flexible**: Supports domain, subdomain, and path-based access ✅ **Maintainable**: Clear separation of concerns, follows established patterns ✅ **User-Friendly**: Responsive design, theme integration, proper UX flows The key architectural decision was recognizing that customers and users are fundamentally different entities requiring separate authentication flows, token structures, and database models.