347 lines
13 KiB
Markdown
347 lines
13 KiB
Markdown
# Identity Model
|
|
|
|
## Purpose
|
|
|
|
This document describes the central identity model managed by the Identity Platform. It is the single source of truth for **who a user is** and **whether they are authenticated**.
|
|
|
|
UserManager answers:
|
|
|
|
- **WHO ARE YOU?** → User identity (UUID, email, phone)
|
|
- **ARE YOU AUTHENTICATED?** → Session + token validity
|
|
- **IS YOUR IDENTITY VERIFIED?** → email_verified, phone_verified, mfa_enabled
|
|
- **WHICH ORGANIZATIONS?** → Membership (user ↔ Organization)
|
|
- **WHICH PRODUCTS?** → Product catalog + active context in JWT
|
|
- **WHAT SESSIONS?** → Session tracking (browser/api/device)
|
|
|
|
Products answer:
|
|
|
|
- **WHAT CAN YOU DO HERE?** → Product-domain permissions (roles, policies) within a specific context
|
|
|
|
---
|
|
|
|
## Entity: User
|
|
|
|
**Definition:** The central identity entity. A User is a person whose identity is managed by the Identity Platform. The User's UUID is **shared across all products** — no product creates a separate user for the same person.
|
|
|
|
**Key Principle:** A User's identity is **immutable** and **product-agnostic**. A User can be an Owner in one org, an Employee in another, and a Specialist in Hamsoo — all as the same User entity.
|
|
|
|
### User Schema
|
|
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| `id` | UUID | PK, immutable | Globally unique user identifier. Shared across all products. |
|
|
| `email` | EmailField | unique, nullable | Primary authentication channel. Verified. |
|
|
| `phone` | CharField | unique, nullable | Secondary authentication channel. Verified. |
|
|
| `username` | SlugField | unique, nullable | Human-readable identifier. |
|
|
| `full_name` | CharField | blank | Display name |
|
|
| `given_name` | CharField | blank | First name |
|
|
| `family_name` | CharField | blank | Last name |
|
|
| `avatar_url` | URLField | blank | Profile picture URL |
|
|
| `status` | CharField | choices | Pending / Active / Suspended / Banned |
|
|
| `email_verified` | BooleanField | default False | Is email verified |
|
|
| `phone_verified` | BooleanField | default False | Is phone verified |
|
|
| `locale` | CharField | default "fa" | Default locale (fa/en) |
|
|
| `timezone` | CharField | default "UTC" | User's timezone |
|
|
| `mfa_enabled` | BooleanField | default False | Is MFA enrolled |
|
|
| `totp_secret` | CharField | blank | TOTP secret (encrypted) |
|
|
| `password_reset_token` | CharField | blank | Hash of password reset token |
|
|
| `password_reset_expires` | DateTimeField | nullable | Reset token expiry |
|
|
| `last_login_at` | DateTimeField | nullable | Last login timestamp |
|
|
| `is_active` | BooleanField | default True | Django auth active flag |
|
|
| `is_staff` | BooleanField | default False | Django admin access |
|
|
| `created_at` | DateTimeField | auto_now_add | Creation timestamp |
|
|
| `updated_at` | DateTimeField | auto_now | Update timestamp |
|
|
|
|
**Removed (previously product-specific):**
|
|
- `employer` / `job_seeker` global roles → These are **product-specific roles**, managed by Bermooda/Hamsoo, NOT identity layer.
|
|
- Any product-specific profile fields → Managed by Product layers (BusinessProfile, Workspace, etc.)
|
|
|
|
### Computed Properties
|
|
|
|
| Property | Type | Description |
|
|
|----------|------|-------------|
|
|
| `user_id` | UUID | Alias for `id` — the canonical user identifier |
|
|
| `identity_key` | str | `usr_{id_hex}` — prefixed identifier for external systems |
|
|
|
|
### User Lifecycle
|
|
|
|
```
|
|
1. Created (status=pending) → User registered, not yet active
|
|
2. Verified (status=active) → Email/phone verified, user can authenticate
|
|
3. Active → User is fully operational
|
|
4. Suspended → Temporarily blocked (self-serve or admin action)
|
|
5. Banned → Permanently blocked (admin action)
|
|
```
|
|
|
|
**Key Rules:**
|
|
- UUID never changes — even if email/phone changes
|
|
- `employer`/`job_seeker` roles are NOT on the User model
|
|
- Product-specific roles come from Membership → Organization → Product mapping
|
|
- A suspended/banned user cannot authenticate (LoginView rejects them)
|
|
|
|
---
|
|
|
|
## Entity: Organization
|
|
|
|
**Definition:** A generic organization (company, team, workspace). NOT tied to any specific product.
|
|
|
|
**Key Principle:** Organization is a generic container. `Bermooda.Workspace` and `Hamsoo.BusinessProfile` are product-layer entities that **map to** an identity.Organization.
|
|
|
|
### Organization Schema
|
|
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| `id` | UUID | PK | Organization identifier |
|
|
| `name` | CharField | required | Display name |
|
|
| `slug` | SlugField | unique, auto-generated | URL-safe identifier |
|
|
| `description` | TextField | blank | Description |
|
|
| `logo_url` | URLField | blank | Logo |
|
|
| `website_url` | URLField | blank | Website |
|
|
| `status` | CharField | choices (active/inactive) | Active/Inactive |
|
|
| `created_by` | FK→User | nullable | User who created org |
|
|
| `created_at` / `updated_at` | DateTimeField | auto | Timestamps |
|
|
|
|
**No product-specific fields.** A Bermooda Workspace adds fields like `industry` via a product-layer FK to Organization.
|
|
|
|
### Example: Bermooda → Workspace mapping
|
|
|
|
```python
|
|
# In Bermooda product layer
|
|
class Workspace(models.Model):
|
|
org = models.ForeignKey("identity.Organization", on_delete=models.CASCADE)
|
|
industry = models.CharField(max_length=100)
|
|
hiring_settings = JSONField()
|
|
...
|
|
```
|
|
|
|
The Workspace is NOT an Organization — it extends one.
|
|
|
|
---
|
|
|
|
## Entity: Membership
|
|
|
|
**Definition:** The relationship between a User and an Organization. Carries role context.
|
|
|
|
**Key Principle:** Roles are **contextual** — a role on the Membership (org-level owner/admin/member), not a global role on the User. Product-specific roles are in the Product layer.
|
|
|
|
### Membership Schema
|
|
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| `id` | UUID | PK | Membership identifier |
|
|
| `user` | FK→User | required | User identity |
|
|
| `organization` | FK→Organization | required | Organization context |
|
|
| `role` | CharField | choices (owner/admin/member) | Org-level role |
|
|
| `status` | CharField | choices (active/invited/pending/deactivated) | Membership state |
|
|
| `invited_by` | FK→User | nullable | Who invited |
|
|
| `joined_at` | DateTimeField | nullable | When accepted |
|
|
| `created_at` / `updated_at` | DateTimeField | auto | Timestamps |
|
|
|
|
**Constraints:**
|
|
- `UniqueConstraint(fields=["user", "organization"])` — a user can have only one membership per organization
|
|
|
|
### Role Context (Key Concept)
|
|
|
|
```
|
|
User: Ali
|
|
Org: ACME
|
|
Membership: role=owner → Ali is Owner of ACME (identity-level)
|
|
|
|
Bermooda: maps ACME → Workspace
|
|
Bermooda role: Ali's profile in Bermooda's Workspace (e.g., HR Manager) — product-layer
|
|
|
|
Hamsoo: not connected to ACME
|
|
Hamsoo profile: Ali's specialist rating, bio — product-layer
|
|
```
|
|
|
|
Products read the Membership to know **if** the user has access to an org, then apply **their own** product-specific logic for **what they can do** within it.
|
|
|
|
---
|
|
|
|
## Entity: Product
|
|
|
|
**Definition:** A registered product in the identity ecosystem. Identity uses this to know which products exist and are active.
|
|
|
|
**Key Principle:** Identity knows that `bermooda` and `hamsoo` are products. It does NOT store product-domain data (BusinessProfile, projects, payroll). It only stores the product's identity metadata.
|
|
|
|
### Product Schema
|
|
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| `id` | UUID | PK | Product identifier |
|
|
| `key` | SlugField | unique | Unique product key (bermooda, hamsoo, ...) |
|
|
| `name` | CharField | required | Human-readable name |
|
|
| `description` | TextField | blank | Description |
|
|
| `logo_url` | URLField | blank | Product logo |
|
|
| `website_url` | URLField | blank | Product website |
|
|
| `is_active` | BooleanField | default True | Can authenticate against this product |
|
|
| `created_by` | FK→User | nullable | Who registered |
|
|
| `created_at` / `updated_at` | DateTimeField | auto | Timestamps |
|
|
|
|
---
|
|
|
|
## Entity: Application (OAuth2 Client)
|
|
|
|
**Definition:** An OAuth2 client registered against a specific Product. Used for authentication flows.
|
|
|
|
### Relationship to Product
|
|
|
|
Each Application has a `product_key` that identifies which product it belongs to. This is for routing/context — not for storing product domain data.
|
|
|
|
---
|
|
|
|
## Entity: Session
|
|
|
|
**Definition:** A user's authenticated session. Tracks device, IP, and lifecycle.
|
|
|
|
### Session Lifecycle
|
|
|
|
```
|
|
Created (active) → User logs in
|
|
→ Session created, refresh token issued
|
|
↓
|
|
Active (active) → User uses platform
|
|
↓
|
|
Expired (expired) → Refresh token lifetime (30 days) exceeded
|
|
↓
|
|
Revoked (revoked) → User logs out, password change, admin action
|
|
```
|
|
|
|
Session is identity-layer. It tracks **that** a user authenticated — not what they did in a product.
|
|
|
|
---
|
|
|
|
## Entity: SecurityEvent
|
|
|
|
**Definition:** An immutable audit event. Every auth action, state change, and security-relevant action is logged.
|
|
|
|
### Event Types
|
|
|
|
| Event Type | Severity | Description |
|
|
|------------|----------|-------------|
|
|
| `login_success` | info | Successful login |
|
|
| `login_failed` | medium | Failed login attempt |
|
|
| `login_locked` | high | Account locked due to brute-force |
|
|
| `logout` | low | User logged out |
|
|
| `password_change` | medium | Password changed |
|
|
| `password_reset` | medium | Password reset completed |
|
|
| `email_verified` | low | Email verified |
|
|
| `phone_verified` | low | Phone verified |
|
|
| `mfa_enabled` | low | MFA enrolled |
|
|
| `mfa_disabled` | low | MFA removed |
|
|
| `session_revoked` | medium | Session revoked |
|
|
| `session_expired` | low | Session expired |
|
|
| `session_created` | low | New session created |
|
|
| `device_added` | low | New device registered |
|
|
| `application_created` | medium | OAuth2 app registered |
|
|
| `application_revoked` | high | OAuth2 app revoked |
|
|
|
|
### Severity Levels
|
|
|
|
- `info` — Normal operation
|
|
- `low` — Minor event
|
|
- `medium` — Worth noting, potential investigation
|
|
- `high` — Security-relevant, requires action
|
|
- `critical` — Immediate action required
|
|
|
|
---
|
|
|
|
## JWT Token Structure
|
|
|
|
### Access Token (RS256, 15 min lifetime)
|
|
|
|
```json
|
|
{
|
|
"user_id": "uuid",
|
|
"user_status": "active",
|
|
"identity_key": "usr_abc123...",
|
|
"product_key": "bermooda", // optional — active context
|
|
"organization_id": "uuid", // optional — active context
|
|
"token_type": "access",
|
|
"exp": 1700000000,
|
|
"iat": 1699999100,
|
|
"iss": "https://id.example.com",
|
|
"jti": "unique-token-id"
|
|
}
|
|
```
|
|
|
|
**Claims policy:**
|
|
- Always present: `user_id`, `user_status`, `identity_key`, `exp`, `iat`, `iss`, `jti`
|
|
- Optional (context): `product_key`, `organization_id` — only included if login established a context
|
|
- **Never present**: product-specific roles, permissions, business data
|
|
|
|
### Refresh Token (RS256, 30 day lifetime)
|
|
```json
|
|
{
|
|
"user_id": "uuid",
|
|
"session_id": "uuid",
|
|
"token_type": "refresh",
|
|
"exp": 1702591100,
|
|
"iat": 1699999100,
|
|
"iss": "https://id.example.com",
|
|
"jti": "unique-token-id"
|
|
}
|
|
```
|
|
|
|
**Token rotation:** Each refresh issues a new refresh token; the old one is revoked and stored in `replaced_by`. Products must discard old refresh tokens immediately.
|
|
|
|
### ID Token (OIDC)
|
|
```json
|
|
{
|
|
"sub": "uuid",
|
|
"name": "User Name",
|
|
"email": "user@acme.com",
|
|
"email_verified": true,
|
|
"preferred_username": "user",
|
|
"locale": "fa",
|
|
"iat": 1699999100,
|
|
"exp": 1700000000,
|
|
"iss": "https://id.example.com",
|
|
"aud": "bermooda-client-id"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Verification
|
|
|
|
**Definition:** Separate from authentication, verification confirms the authenticity of identity claims.
|
|
|
|
```
|
|
Authentication: "You have a password to the account."
|
|
Verification: "This email/phone belongs to you. This identity is real."
|
|
```
|
|
|
|
### Verification States
|
|
|
|
| Channel | Verified? | Verified At |
|
|
|---------|-----------|-------------|
|
|
| Email | boolean | timestamp |
|
|
| Phone | boolean | timestamp |
|
|
|
|
`Verification` is identity-layer. Future extensions:
|
|
- Email verification → Identity Platform
|
|
- Phone verification → Identity Platform
|
|
- Shahkar (Iran national ID verification) → Identity Platform (verification provider)
|
|
- Manual Verification → Identity Platform (admin override)
|
|
|
|
In the future, products may request additional verification (e.g., Hamsoo needs business license verified) — but that's a Product-layer concern.
|
|
|
|
---
|
|
|
|
## User ID Consistency
|
|
|
|
**Guarantee:** A user's UUID is the same across Bermooda, Hamsoo, and all future products.
|
|
|
|
```
|
|
UserManager: user_id = UUID(550e8400-e29b-41d4-a716-446655440000)
|
|
|
|
Bermooda: user_id = UUID(550e8400-e29b-41d4-a716-446655440000) ← same
|
|
Hamsoo: user_id = UUID(550e8400-e29b-41d4-a716-446655440000) ← same
|
|
|
|
No product creates its own user record.
|
|
No product duplicates user identity.
|
|
```
|
|
|
|
Products reference the identity.User UUID via FK. They never store passwords, sessions, or tokens.
|