- 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>
20 KiB
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:
- No styled authentication pages for customers
- Customer authentication was incorrectly trying to use the User model (admins/stores)
- Cookie paths were hardcoded and didn't work with multi-access routing (domain, subdomain, path-based)
- 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
rolefield (admin/store) - Have
usernamefield - Managed via
app/services/auth_service.py
- Have
-
Customers (
models/database/customer.py): Storefront customers- Store-scoped (each store has independent customers)
- No
roleorusernamefields - Have
customer_number,total_orders, store relationship - Managed via
app/services/customer_service.py
2. JWT Token Structure
Customer tokens have a distinct structure:
{
"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
-
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
-
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
-
app/templates/storefront/account/forgot-password.html- Password reset request page
- Two-state UI (form → success)
- Email validation
-
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:
class UserLogin(BaseModel):
username: str
password: str
After:
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- Changeduser_credentials.usernametouser_credentials.email_or_usernameapp/api/v1/admin/auth.py- Updated logging to useemail_or_usernamestatic/admin/js/login.js- Sendemail_or_usernamein payloadstatic/store/js/login.js- Sendemail_or_usernamein payload
Files Modified
1. app/api/v1/storefront/auth.py
Changes:
- Added
CustomerLoginResponsemodel (usesCustomerResponseinstead ofUserResponse) - Updated
customer_loginendpoint to:- Calculate cookie path dynamically based on store access method
- Set cookie with correct path for multi-access support
- Return
CustomerLoginResponsewith proper customer data
- Updated
customer_logoutendpoint to calculate cookie path dynamically
Key Code:
# 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_customerto create JWT tokens directly usingauth_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:
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:
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
UsertoCustomer - Updated all protected route handlers:
- Changed parameter type from
current_user: Usertocurrent_customer: Customer - Updated function calls from
user=current_usertouser=current_customer
- Changed parameter type from
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:
# 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
UserLoginschema to acceptemail_or_usernameinstead ofusername - 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
-
User loads login page →
GET /storefront/orion/account/login- Middleware detects store from path
- Sets
detection_method = "path"in store_context - Renders login template
-
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_tokencookie with correct path - Returns token + customer data
-
Browser redirects to dashboard →
GET /storefront/orion/account/dashboard- Browser sends
customer_tokencookie (path matches!) - Dependency
get_current_customer_from_cookie_or_headerextracts token - Decodes JWT, validates
type == "customer" - Loads Customer from database
- Renders dashboard with customer data
- Browser sends
Logout Flow
-
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
-
User confirms logout →
POST /api/v1/storefront/auth/logout- Calculates cookie path (same logic as login)
- Deletes cookie with matching path
- Returns success message
-
Frontend redirects to login page
- Shows success toast notification
- Clears localStorage token
- Redirects after 500ms delay
Security Features
Cookie Security
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:
{% extends "storefront/base.html" %}
{% block title %}Page Title{% endblock %}
{% block alpine_data %}componentName(){% endblock %}
{% block content %}
<!-- Page content -->
{% endblock %}
{% block extra_scripts %}
<script>
function componentName() {
return {
...storefrontLayoutData(),
// Component-specific data/methods
}
}
</script>
{% 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:
<!-- Modal trigger -->
<button @click="showLogoutModal = true">Logout</button>
<!-- Modal component -->
<div x-show="showLogoutModal" x-cloak class="fixed inset-0 z-50">
<!-- Backdrop with fade animation -->
<div x-show="showLogoutModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@click="showLogoutModal = false"
class="fixed inset-0 bg-gray-500 bg-opacity-75">
</div>
<!-- Modal panel with slide+scale animation -->
<div x-show="showLogoutModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
class="bg-white dark:bg-gray-800 rounded-lg">
<!-- Modal content -->
</div>
</div>
Alpine.js Component:
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
- Customer can register new account
- Customer can login with email/password
- Admin can login with username (using unified schema)
- Store can login with username (using unified schema)
- Cookie is set with correct path for path-based access
- Cookie is sent on subsequent requests to dashboard
- Customer authentication dependency validates token correctly
- Dashboard loads with customer data
- Customer can logout
- Logout confirmation modal displays correctly
- Modal has smooth animations and transitions
- Modal supports dark mode
- Toast notification shows on logout
- Cookie is properly deleted on logout
- Unauthorized access redirects to login
- Theme styling is applied correctly
- Dark mode works
- Mobile responsive layout
- Admin/store login button spinner aligns correctly
Known Limitations
-
Password Reset Not Implemented:
forgot-passwordandreset-passwordendpoints are placeholders (TODO comments in code) -
Email Verification Not Implemented: Customers are immediately active after registration
-
Session Management: No refresh tokens, single JWT with 30-minute expiry
-
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
-
Password Reset Flow:
- Generate secure reset tokens
- Send password reset emails
- Token expiry and validation
- Password reset form
-
Email Verification:
- Send verification email on registration
- Verification token validation
- Resend verification email
-
Account Management:
- Edit profile (name, email, phone)
- Change password
- Manage addresses (CRUD)
- View order history with filtering/search
- Order tracking
-
Security Enhancements:
- Refresh tokens for longer sessions
- Rate limiting on login/registration
- Account lockout after failed attempts
- 2FA/MFA support
-
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 signingJWT_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.jsstatic/shared/js/icons.jsstatic/storefront/js/storefront-layout.jsstatic/shared/js/utils.jsstatic/shared/js/api-client.jsstatic/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_methodis 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.