13 KiB
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_seekerglobal 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_seekerroles 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
# 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 operationlow— Minor eventmedium— Worth noting, potential investigationhigh— Security-relevant, requires actioncritical— Immediate action required
JWT Token Structure
Access Token (RS256, 15 min lifetime)
{
"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)
{
"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)
{
"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 |
|---|---|---|
| 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.