# 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.