Skip to content

Auth & sessions ​

Credentials + mandatory OTP. No OIDC/SAML. No demo bypass (LOCUS_DEMO_MODE removed; there are no demo accounts).

text
browser cookie locus_session (signed ticket)
  → Edge middleware (presence + HMAC signature + expiry)
  → BFF requireBffSession + enforceRbacGuard   (loads SoR session)
  → C# InstitutionalAuthMiddleware             (AuthSessions → ClaimsPrincipal)
  → TenantRlsMiddleware                        (GUCs for FORCE RLS)
  → controllers: InstitutionalAuthorization.RequirePermission

Bank (locus-web) ​

text
Register → Organization + INSTITUTION_ADMIN → OTP challenge
Login → always mfaRequired → OTP → AuthSessions + HMAC locus_session cookie
Logout → revoke AuthSessions + clear cookie
  • No OIDC/SAML. No demo bypass.
  • Edge middleware verifies ticket signature + expiry (SESSION_SIGNING_KEY).
  • BFF forwards Authorization: Bearer <sessionId> + X-Organization-Id from session claims.

Sequence ​

mermaid
sequenceDiagram
    autonumber
    actor U as User
    participant W as locus-web /api/auth
    participant S as SoR /api/auth/*
    participant E as Edge middleware
    U->>W: POST {action: "register", email, password, fullName, institutionName, country, institutionType}
    W->>S: POST /api/auth/register
    S->>S: guards, Organization + User + Membership(INSTITUTION_ADMIN), audit ORG_REGISTERED
    S-->>W: {mfaRequired: true, organizationId, otpCode? (Development only)}
    U->>W: POST {action: "login", email, password, rememberDevice}
    W->>S: POST /api/auth/login
    S-->>W: {mfaRequired: true, portal: "institution"}
    U->>W: POST {action: "otp", email, code}
    W->>S: POST /api/auth/otp
    S->>S: AuthSessions row sess_<guid>, TokenHash = SHA-256(sessionId), audit SIGN_IN
    S-->>W: {session: {sessionId, tenantId, role, expiresAt, …}}
    W->>W: mintSessionTicket(sessionId, expiresAt)
    W-->>U: Set-Cookie locus_session=v1.<payload>.<sig>; HttpOnly; SameSite=Lax; Secure(prod)
    U->>E: GET /workspace/overview
    E->>E: verifySessionTicket (signature + exp)
    E-->>U: allow or 302 /login?redirect=…

/api/auth actions (web BFF) ​

Method + actionBehaviour
GETResolve cookie → fetchSorSession; 401 { authenticated: false } if no session
POST registerregisterAgainstSor; requires email, password, fullName, institutionName; returns mfaRequired: true
POST loginloginAgainstSor; always mfaRequired (a response without it is a 500)
POST otpverifyOtpAgainstSor → mint ticket → cookie. Recovery codes are not supported (400)
POST logoutRevokes AuthSessions in the SoR; cookie deleted even if the SoR is unreachable
POST switch_tenantaudited tenant switch (domain mirror)

otpCode is returned in API responses only outside production (and logged by the SoR in Development).

Session ticket v1.<payload>.<sig> ​

Format (session-ticket.ts, Edge-safe WebCrypto):

text
v1.<base64url(JSON {"sid": "<sessionId>", "exp": <unix seconds>})>.<base64url(HMAC-SHA256(key, "v1." + payload))>
PartDetail
sidOpaque SoR session id (sess_<guid>); the SoR stores only SHA-256(sessionId) as TokenHash
expUnix seconds; exp*1000 <= now → EXPIRED
KeySESSION_SIGNING_KEY; required in production (throws otherwise, verify returns MISSING_KEY); dev fallback key only outside production
Verificationconstant-time compare; failure reasons MALFORMED, BAD_SIGNATURE, EXPIRED, MISSING_KEY
LifetimeSoR session 8 h, or 72 h with rememberDevice
Cookielocus_session (also __Secure-locus-session read), httpOnly, secure in production, sameSite=lax, path=/, maxAge = exp − now
Legacyunsigned sess_* cookies are always rejected

The same format is used by locus-ops with cookie locus_ops_session (separate cookie; same key recommended).

Public routes (bank middleware) ​

ClassPaths
Pages/, /landing, /login, /register, /auth/login, /auth/forgot-password, /auth/otp, /auth/reset-password, /pricing, /request-access, /request-access/verify, /onboarding, /auth/accept-invite, /legal, /legal/{mentions-legales,confidentialite,conditions,securite}
API prefixes/api/onboarding, /api/regulatory/search, /api/auth, /api/locus/members/accept-invite, /api/locus/access-requests
Everything else under /api/**401 UNAUTHENTICATED without a valid signed ticket
/workspace/**redirect /login?redirect=…

Error codes: 401 UNAUTHENTICATED, 401 SESSION_EXPIRED, 403 TENANT_FORBIDDEN.

SoR endpoints ​

EndpointPurpose
POST /api/auth/registerOrg + user + INSTITUTION_ADMIN membership; OTP e-mail
POST /api/auth/loginPassword step; always OTP
POST /api/auth/platform-loginOps password step
POST /api/auth/otpIssues AuthSession
POST /api/auth/logoutIsRevoked = true, audit SIGN_OUT
GET /api/auth/sessionSession details from claims (sub, tenant_id, …)

Registration guards (SoR AuthController.Register): all of email, password, fullName, institutionName required; consumer e-mail rejected; password ≥ 12 characters; 409 EMAIL_TAKEN; 409 ORG_EXISTS when an active org already owns the corporate domain (ask an admin for an invitation). New org defaults: edition securities, primary country CG unless provided.

Login guards: invalid credentials → generic Identifiants invalides.; Invited membership must accept the invite first; Suspended → access suspended. Fresh tenant = empty workspace until the first dossier.

Platform (locus-ops) ​

text
POST /api/auth/platform-login → rejects non-PLATFORM_OPERATOR before OTP
OTP → locus_ops_session cookie
RuleBehaviour
platform-login rejectSoR returns 401 NOT_PLATFORM_OPERATOR ("Ce portail est réservé aux opérateurs plateforme Locus.") before an OTP is generated
Bank login by an operatorSoR returns 401 USE_PLATFORM_LOGIN
OTP stepops BFF additionally returns 403 NOT_PLATFORM_OPERATOR if the issued session role is not PLATFORM_OPERATOR
Session readGET /api/auth returns 401 unless role is PLATFORM_OPERATOR
Public/login, /login/otp, /api/auth only

Seeded (seeder only): ops@locus.platform / Locus@2026!Seed — only when Seed:RunOnStartup=true; never in production.

Access requests ​

Public submit/verify on bank BFF → SoR state machine. Approve / provision UI on ops /access-requests (system:manage).

text
SUBMITTED ─(24 h magic token)─► EMAIL_VERIFIED ─► COMPLIANCE_REVIEW ─► APPROVED ─► PROVISIONED
                                                        └────────────► REJECTED
RuleValue
Free e-mail domainsrejected → CONSUMER_EMAIL_REJECTED
TermstermsAccepted + advisoryDisclaimerAccepted required → TERMS_NOT_ACCEPTED
Rate limit5 requests / 15 min per address → 429 RATE_LIMIT_EXCEEDED
Domain dedupeactive org → TENANT_ALREADY_PROVISIONED; pending request → PENDING_REQUEST_EXISTS
Verify token24 h TTL; SHA-256(token) stored; link {App:PublicWebBaseUrl}/request-access/verify?token=…
Review / provisionsystem:manage (PLATFORM_OPERATOR); provision creates Organization + edition modules + invited INSTITUTION_ADMIN

Routes: BFF /api/locus/access-requests (submit public; list = system:manage), /verify (public), /[id]/review, /[id]/provision; SoR api/access-requests (GET, POST, verify, {id}/review, {id}/provision). See Access requests (ops).

Roles (RBAC) ​

RoleScope
PLATFORM_OPERATORGlobal platform (system:manage): health, tenant provisioning, vault, cross-org market ingest. Cannot be invited as an in-tenant role via Members API
INSTITUTION_ADMINTenant admin: users, seats, edition, module toggles, audit
ARRANGER_LEADDossier creation, term sheets, memo sign-off, external sharing
CREDIT_ANALYSTSYSCOHADA reconciliation, covenant stress, cell overrides with rationale; no memo sign-off (403)
COMPLIANCE_OFFICERCOSUMAF pre-visa, legal cross-checks; no financial override (403)
SYNDICATE_GUESTRead-only deal room; no memo generation (403)

InstitutionalRole also carries expanded desk roles (ORIGINATION_BANKER, RISK_MANAGER, LEGAL_COUNSEL, RESEARCH_ANALYST, PORTFOLIO_MANAGER, COMMITTEE_VIEWER). Matrix: locus-backend/Services/InstitutionalAuthorization.cs + TS mirrors in src/domains/auth/.

Fail-closed behaviours ​

  • BFF prefers SoR session authority; alerts / reconciliation / overrides fail closed (no DOMAIN_MIRROR fallback).
  • C# deal / org / member / export / document / market / portfolio / search / precedent / blocker / analysis-job paths require an authenticated tenant + permission (no seed-org fallback).
  • Cross-tenant dossier access → 403 TENANT_FORBIDDEN / not found under the caller's OrganizationId.
  • Auth:AllowDevHeaders must be false in production (Development only: X-Organization-Id / X-User-*).

Tests: auth_otp_and_register, session_ticket_middleware, auth_gate_and_api_rbac, fail_closed_integration, onboarding_lifecycle, access_request_production_api.

Source of trust for Locus. If this conflicts with code, code + IMPLEMENTATION_TRACKER.md win.