Skip to main content

PRD-001: Identity & Auth

Author: MLT backend team | Date: 2026-08-18 | Status: Draft | Version: v0.2.1


Revision History

VersionDateAuthorDescription of Change
v0.1.02026-08-18MLT backend teamInitial draft from the rebuild design discussion (roadmap E02·E1).
v0.2.02026-08-18MLT backend teamReconciled with the 2026-08-11 client meeting + firm brief: onboarding is operator-provisioned (no self-service); super_admin is an Account-level platform role, not a CompanyRole; CompanyRole expanded to the firm's full invitable ladder; no company switcher; credential-delivery left open.
v0.2.12026-08-18MLT backend teamPassword hashing corrected from ASP.NET PasswordHasher to Argon2id (SYS-REQ-101, §3.2, §11), matching the implementation shipped for Task #19. Both sit behind IPasswordHasher; Argon2id is the stronger default.

1. Context & Business Rationale

MyLegalTeam (MLT) is the umbrella brand over two products — My Legal Channel (whistleblowing, the template this codebase forked from) and My Workplace Legal (workplace disciplinary procedures, what we are building) — kept separate for now. This PRD covers the shared identity, tenancy, and RBAC foundation.

Every other feature — cases, the workflow, evidence, the dossier — depends on knowing who the caller is, which company (tenant) they act in, and what they may do. The backend today has cookie-auth plumbing (the scheme, 401/403 JSON handling, an unwired rate-limit policy) but no account, tenant, or role model — the only persisted entity is Dummy.

The vibecoded PoC got the trust model wrong in three ways this PRD deliberately fixes: its effective role was the global maximum across all companies (privilege bleed), the active tenant was in-memory only, and roles were client-supplied on self-insert (a user could make themselves super_admin). See the PoC autopsy: role is global-max, company context in memory, self-provision escalation.

Sourcing: decisions here are grounded in the firm brief (_poc/firm-qa.md) and the 2026-08-11 client meeting (_poc/meeting-notes-2026-08-11.md).

2. Problem Statement

There is no way to create a user, associate them with a company, assign a role, or enforce any of it. Without a server-authoritative identity + tenancy + RBAC layer, no case data can be safely stored or scoped.


3. Goals, Non-Goals, and Success Metrics

3.1 Goals

  • A server-owned Account (identity), Company (tenant), and Membership (Account × Company × CompanyRole) model.
  • Per-tenant authorization — a caller's permissions are resolved for the active company only, never global-max.
  • Operator-provisioned onboardingsuper_admin (Miew) provisions a company and its first hr_admin; other users join by invite + activation. Roles are never client-chosen.
  • A canonical role model that the whole platform keys off.

3.2 Non-Goals

  • Public self-registration / self-service company creation — the 2026-08-11 meeting decided sales are consultive and onboarding is manual; there is no auto-registration.
  • Case-team / case-role authorization (appointing instructor/reviewer/decider to a case) — a follow-up epic, blocked by the Case aggregate.
  • A company switcher UI — Membership is a multi-company-capable join, but there is no switcher now.
  • Social login / external IdP (custom Account aggregate + Argon2id password hashing for now).
  • Portal (arguido / counsel / witness) access — token-based, a separate epic.

3.3 Success Metrics

Metric TypeDefinitionBaselineTarget
PrimaryAn operator can provision a company; its users log in and are correctly authorized within itnone (no model)works end-to-end
Guard RailNo request can read/write another tenant's data0 cross-tenant leaks (proven by test)
Guard RailNo client can self-assign a role above what they may grant0 escalation paths

4. Roles & Actors

  • super_admin — the platform operator (Miew). An Account-level capability, cross-tenant, checked independently of any Membership (NOT a CompanyRole — a per-tenant join can't hold a global role; that was the PoC's bug). Provisions companies, assigns any role including hr_admin, sees across tenants. Seeded out-of-band.
  • CompanyRole (per-tenant, on a Membership) = { hr_admin, case_manager, instructor_internal, instructor_external, legal_reviewer, decider } — the firm's invitable ladder. The last four define what a person is in the company; case_team_members then scopes them to the specific cases they are appointed to.
  • Portal partiesemployee/arguido, witness, external_counsel — are not app roles; they access via token (separate epic).

5. User Stories

  • As the operator, I want to provision a company and its first HR admin so a client can start.
  • As a user, I want to log in and out so I can access my company's workspace.
  • As an invited user, I want to set my password via an activation link so I can get in securely.
  • As an HR admin, I want to invite teammates (a list of email + role) so they can work in my company.
  • As any user, I want the system to show me only my company's data so tenants stay isolated.
  • As a forgetful user, I want to reset my password so I can recover access.

6. System Requirements

6.1 Identity

  • SYS-REQ-101: An Account is pure identity (email, password hash via Argon2id, active flag, security stamp). It carries no role.
  • SYS-REQ-102: The login cookie holds identity only — account id + security stamp; not role or tenant. Role/tenant are resolved per request, fresh.
  • SYS-REQ-103: Endpoints: login, logout. No public registration — accounts exist only via provisioning/invite. Login is rate-limited via the existing login-attempts policy.
  • SYS-REQ-104: Provisioned/invited accounts are inactive until they set a first password via a single-use, expiring activation link (which verifies email ownership). Password reset uses the same token mechanism. Completing activation/reset rotates the security stamp.

6.2 Tenancy

  • SYS-REQ-201: A Membership = Account × Company × CompanyRole is the single source of truth for tenancy and role.
  • SYS-REQ-202: The active company is carried per request (route segment or X-Company-Id), validated against the caller's memberships; a single-membership account defaults automatically.
  • SYS-REQ-203: Every tenant-owned entity is constrained to the active company by an EF Core global query filter; writes stamp company-id from context, never the client body.

6.3 RBAC & provisioning

  • SYS-REQ-301: Permissions are resolved from the active company's membership — never global-max. super_admin is the only cross-tenant role, checked at the Account level.
  • SYS-REQ-302: Only super_admin provisions a company, setting its first hr_admin. There is no self-service company creation.
  • SYS-REQ-303: An hr_admin invites users (a list of distinct email + CompanyRole); the server validates each grant. Only super_admin may grant hr_admin.
  • SYS-REQ-304: A company has one hr_admin by default; additional ones are super_admin-only (not a hard cap).

7. Workflow

super_admin provisions Company + first hr_admin ──► hr_admin invited ──► activates (sets password)


hr_admin invites users (email + role) ──► each activates ──► logs in


Every request: cookie = identity → request carries company → membership resolved (cached)
→ CompanyRole → authorized + tenant-scoped

8. Open Questions

  • Credential delivery (to decide before build): activation link (default), magic-link / passwordless, SSO, or a random per-user temp password. Invariants regardless: never a shared/known credential; never auto-confirm without proving inbox ownership.
  • Confirm the canonical mapping target for the legacy viewer role (likely dropped).
  • Does an hr_admin who leaves strand a company (only super_admin can mint a replacement)? — acceptable under consultive onboarding, but confirm.

9. Edge Cases & Exception Handling

(to be extended by refine-feature)

10. Given-When-Then Acceptance Criteria

(per-task acceptance criteria live on each Task issue; see the Epic's sub-issues)

11. Technical Constraints & Dependencies

  • .NET Clean Architecture + CQRS; cookie auth already scaffolded; Argon2id password hashing (Konscious.Security.Cryptography.Argon2), behind the IPasswordHasher abstraction.
  • EF Core global query filter for tenant scoping; membership resolution cached with short TTL.
  • No case-team authorization until the Case aggregate exists (theme E03).
  • Activation/reset/invite emails depend on the transactional email pipeline (theme E10).