255 lines
6.7 KiB
Markdown
255 lines
6.7 KiB
Markdown
# Authentication & Authorization
|
|
|
|
**Last Verified:** December 25, 2025
|
|
**Backend Path:** `backend/igny8_core/auth/`
|
|
**Frontend Path:** `frontend/src/store/authStore.ts`
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
| What | File | Key Functions |
|
|
|------|------|---------------|
|
|
| User Model | `auth/models.py` | `User`, `Account`, `Plan` |
|
|
| Auth Views | `auth/views.py` | `LoginView`, `RegisterView`, `RefreshTokenView` |
|
|
| Middleware | `auth/middleware.py` | `AccountContextMiddleware` |
|
|
| JWT Auth | `api/authentication.py` | `JWTAuthentication`, `CookieJWTAuthentication` |
|
|
| API Key Auth | `api/authentication.py` | `APIKeyAuthentication` |
|
|
| Frontend Store | `store/authStore.ts` | `useAuthStore` |
|
|
|
|
---
|
|
|
|
## Authentication Methods
|
|
|
|
### 1. JWT Token Authentication (Primary)
|
|
|
|
**Flow:**
|
|
1. User logs in via `/api/v1/auth/login/`
|
|
2. Backend returns `access_token` (15 min) + `refresh_token` (7 days)
|
|
3. Frontend stores tokens in localStorage and Zustand store
|
|
4. All API requests include `Authorization: Bearer <access_token>`
|
|
5. Token refresh via `/api/v1/auth/token/refresh/`
|
|
|
|
**Token Payload:**
|
|
```json
|
|
{
|
|
"user_id": 123,
|
|
"account_id": 456,
|
|
"email": "user@example.com",
|
|
"exp": 1735123456,
|
|
"iat": 1735122456
|
|
}
|
|
```
|
|
|
|
### 2. Session Authentication (Admin/Fallback)
|
|
|
|
- Used by Django Admin interface
|
|
- Cookie-based session with CSRF protection
|
|
- Redis-backed sessions (prevents user swapping bug)
|
|
|
|
### 3. API Key Authentication (WordPress Bridge)
|
|
|
|
**Flow:**
|
|
1. Account generates API key in settings
|
|
2. WordPress plugin uses `Authorization: ApiKey <key>`
|
|
3. Backend validates key, sets `request.account` and `request.site`
|
|
|
|
**Use Cases:**
|
|
- WordPress content sync
|
|
- External integrations
|
|
- Headless CMS connections
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
| Method | Path | Handler | Purpose |
|
|
|--------|------|---------|---------|
|
|
| POST | `/api/v1/auth/register/` | `RegisterView` | Create new user + account |
|
|
| POST | `/api/v1/auth/login/` | `LoginView` | Authenticate, return tokens |
|
|
| POST | `/api/v1/auth/logout/` | `LogoutView` | Invalidate tokens |
|
|
| POST | `/api/v1/auth/token/refresh/` | `RefreshTokenView` | Refresh access token |
|
|
| POST | `/api/v1/auth/password/change/` | `ChangePasswordView` | Change password |
|
|
| POST | `/api/v1/auth/password/reset/` | `RequestPasswordResetView` | Request reset email |
|
|
| POST | `/api/v1/auth/password/reset/confirm/` | `ResetPasswordView` | Confirm reset with token |
|
|
|
|
---
|
|
|
|
## User Roles
|
|
|
|
| Role | Code | Permissions |
|
|
|------|------|-------------|
|
|
| **Developer** | `developer` | Full access across ALL accounts (superuser) |
|
|
| **Admin** | `admin` | Full access to own account |
|
|
| **Manager** | `manager` | Manage content, view billing |
|
|
| **Editor** | `editor` | AI content, manage clusters/tasks |
|
|
| **Viewer** | `viewer` | Read-only dashboards |
|
|
| **System Bot** | `system_bot` | System automation (internal) |
|
|
|
|
**Role Hierarchy:**
|
|
```
|
|
developer > admin > manager > editor > viewer
|
|
```
|
|
|
|
---
|
|
|
|
## Middleware: AccountContextMiddleware
|
|
|
|
**File:** `auth/middleware.py`
|
|
|
|
**Purpose:** Injects `request.account` on every request
|
|
|
|
**Flow:**
|
|
1. Check for JWT token → extract account_id
|
|
2. Check for session → get account from session
|
|
3. Check for API key → get account from key
|
|
4. Validate account exists and is active
|
|
5. Validate plan exists and is active
|
|
6. Set `request.account`, `request.user`
|
|
|
|
**Error Responses:**
|
|
- No account: 403 with JSON error
|
|
- Inactive plan: 402 with JSON error
|
|
|
|
---
|
|
|
|
## Frontend Auth Store
|
|
|
|
**File:** `store/authStore.ts`
|
|
|
|
**State:**
|
|
```typescript
|
|
{
|
|
user: User | null;
|
|
token: string | null;
|
|
refreshToken: string | null;
|
|
isAuthenticated: boolean;
|
|
}
|
|
```
|
|
|
|
**Actions:**
|
|
- `login(email, password)` - Authenticate and store tokens
|
|
- `register(data)` - Create account and store tokens
|
|
- `logout()` - Clear tokens and reset stores
|
|
- `refreshToken()` - Refresh access token
|
|
- `checkAuth()` - Verify current auth state
|
|
|
|
**Critical Implementation:**
|
|
```typescript
|
|
// Tokens are written synchronously to localStorage
|
|
// This prevents race conditions where API calls happen before persist
|
|
localStorage.setItem('auth-storage', JSON.stringify(authState));
|
|
```
|
|
|
|
---
|
|
|
|
## Session Security (Redis-Backed)
|
|
|
|
**Problem Solved:** User swapping / random logout issues
|
|
|
|
**Implementation:**
|
|
```python
|
|
# settings.py
|
|
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
|
SESSION_CACHE_ALIAS = 'default' # Redis
|
|
|
|
# auth/backends.py
|
|
class NoCacheModelBackend(ModelBackend):
|
|
"""Authentication backend without user caching"""
|
|
pass
|
|
```
|
|
|
|
**Session Integrity:**
|
|
- Stores `account_id` and `user_id` in session
|
|
- Validates on every request
|
|
- Prevents cross-request contamination
|
|
|
|
---
|
|
|
|
## API Key Management
|
|
|
|
**Model:** `APIKey` in `auth/models.py`
|
|
|
|
| Field | Type | Purpose |
|
|
|-------|------|---------|
|
|
| key | CharField | Hashed API key |
|
|
| account | ForeignKey | Owner account |
|
|
| site | ForeignKey | Optional: specific site |
|
|
| name | CharField | Key name/description |
|
|
| is_active | Boolean | Enable/disable |
|
|
| created_at | DateTime | Creation time |
|
|
| last_used_at | DateTime | Last usage time |
|
|
|
|
**Generation:**
|
|
- 32-character random key
|
|
- Stored hashed (SHA-256)
|
|
- Shown once on creation
|
|
|
|
---
|
|
|
|
## Permission Checking
|
|
|
|
**In ViewSets:**
|
|
```python
|
|
class MyViewSet(AccountModelViewSet):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
# Automatically filtered by request.account
|
|
return super().get_queryset()
|
|
```
|
|
|
|
**Role Checks:**
|
|
```python
|
|
if request.user.is_admin_or_developer:
|
|
# Admin/developer access
|
|
pass
|
|
elif request.user.role == 'editor':
|
|
# Editor access
|
|
pass
|
|
```
|
|
|
|
---
|
|
|
|
## Logout Flow
|
|
|
|
**Backend:**
|
|
1. Blacklist refresh token (if using token blacklist)
|
|
2. Clear session
|
|
|
|
**Frontend (Critical):**
|
|
```typescript
|
|
logout: () => {
|
|
// NEVER use localStorage.clear() - breaks Zustand persist
|
|
const authKeys = ['auth-storage', 'site-storage', 'sector-storage', 'billing-storage'];
|
|
authKeys.forEach(key => localStorage.removeItem(key));
|
|
|
|
// Reset dependent stores
|
|
useSiteStore.setState({ activeSite: null });
|
|
useSectorStore.setState({ activeSector: null, sectors: [] });
|
|
|
|
set({ user: null, token: null, isAuthenticated: false });
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Common Issues
|
|
|
|
| Issue | Cause | Fix |
|
|
|-------|-------|-----|
|
|
| 403 after login | Tokens not persisted before API call | Write to localStorage synchronously |
|
|
| User swapping | DB-backed sessions with user caching | Redis sessions + NoCacheModelBackend |
|
|
| Token refresh loop | Refresh token expired | Redirect to login |
|
|
| API key not working | Missing site scope | Check API key has correct site assigned |
|
|
|
|
---
|
|
|
|
## Planned Changes
|
|
|
|
| Feature | Status | Description |
|
|
|---------|--------|-------------|
|
|
| Token blacklist | 🔜 Planned | Proper refresh token invalidation |
|
|
| 2FA | 🔜 Planned | Two-factor authentication |
|
|
| SSO | 🔜 Planned | Google/GitHub OAuth |
|