PRD-001: Identity & Auth
Author: MLT backend team | Date: 2026-08-18 | Status: Draft | Version: v0.2.1
Revision History
| Version | Date | Author | Description of Change |
|---|---|---|---|
| v0.1.0 | 2026-08-18 | MLT backend team | Initial draft from the rebuild design discussion (roadmap E02·E1). |
| v0.2.0 | 2026-08-18 | MLT backend team | Reconciled 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.1 | 2026-08-18 | MLT backend team | Password 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 onboarding —
super_admin(Miew) provisions a company and its firsthr_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 Type | Definition | Baseline | Target |
|---|---|---|---|
| Primary | An operator can provision a company; its users log in and are correctly authorized within it | none (no model) | works end-to-end |
| Guard Rail | No request can read/write another tenant's data | — | 0 cross-tenant leaks (proven by test) |
| Guard Rail | No client can self-assign a role above what they may grant | — | 0 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 includinghr_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_membersthen scopes them to the specific cases they are appointed to. - Portal parties —
employee/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-attemptspolicy. - 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_adminis the only cross-tenant role, checked at the Account level. - SYS-REQ-302: Only
super_adminprovisions a company, setting its firsthr_admin. There is no self-service company creation. - SYS-REQ-303: An
hr_admininvites users (a list of distinct email + CompanyRole); the server validates each grant. Onlysuper_adminmay granthr_admin. - SYS-REQ-304: A company has one
hr_adminby default; additional ones aresuper_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
viewerrole (likely dropped). - Does an
hr_adminwho leaves strand a company (onlysuper_admincan 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 theIPasswordHasherabstraction. - 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).