Skip to content

Authentication

Tenant and Central authentication are fully isolated: separate route trees, Sanctum guards, user models, password-reset brokers, and SPA token storage.

Architecture

ConcernTenant ApplicationCentral Application
SPA routes/login, /register, /forgot-password, /reset-password/{token}/central/login, /central/forgot-password, /central/reset-password/{token}
API prefix/api/tenant/v1/api/central/v1
Guardtenant-apicentral-api
User modelApp\Models\User (users)App\Models\CentralUser (central_users)
Password brokeruserscentral_users
SPA token keydc_saas_token_tenantdc_saas_token_central
Token nametenant-tokencentral-token

A tenant login never authenticates a Central administrator, and a Central login never authenticates a tenant user.

/login is the shared tenant entry point. It includes a Workspace field when the browser host does not already resolve a workspace; /central/login is always reserved for platform administrators and never accepts a workspace.

mermaid
flowchart LR
  subgraph SPA
    TAuth["/login /register /reset-password"]
    CAuth["/central/login /central/reset-password"]
    TDash["/dashboard widgets"]
    CAdmin["/central/dashboard …"]
  end

  subgraph API
    TAPI["tenant-api + User"]
    CAPI["central-api + CentralUser"]
    Reg["POST /public/register-workspace"]
  end

  TAuth --> TAPI
  CAuth --> CAPI
  TAuth -->|register| Reg
  Reg --> TAPI
  TAPI --> TDash
  CAPI --> CAdmin

Guards

Defined in config/auth.php:

  • central-api — Sanctum driver, central_users provider
  • tenant-api — Sanctum driver, users provider

Tenant routes also run tenancy + tenant.available. Workspace resolution prefers the request host (including future custom domains), then the authenticated token's workspace, then the submitted workspace value or X-Tenant-Domain header. Central routes run central.domain.

When a Bearer token is present but cannot resolve a workspace (unknown, revoked, or pruned token row), InitializeTenancy returns 401 Unauthenticated so the SPA can redirect to login. A true missing-workspace case with no Bearer still returns 400 with code: workspace_required; the SPA treats that as session expiry on authenticated API calls (not skipAuth) so idle token-clear races hard-redirect to login instead of toasting.

Tenant token TTL comes from the workspace session_lifetime_minutes setting (0 = non-expiring token), falling back to Central when unset. Remember-me still extends a positive TTL to at least 30 days.

Spatie roles/permissions are isolated by guard_name (central-api vs tenant-api).

Registration

POST /api/central/v1/public/register-workspace (honours registration_enabled):

  1. Creates workspace + domain
  2. TenantProvisioningService — billing profile, default-included modules (Leads, Tasks, Communication Templates), authorization defaults, module seed data
  3. TenantAuthBootstrapService::createOwner — owner User with workspace superadmin role (roles must already exist)
  4. Returns Sanctum tenant-token for immediate SPA login

Login and subsequent authenticated requests do not create or repair roles/permissions. See tenant-provisioning.md.

Required body fields: company_name, owner_name, email, password, password_confirmation.

When registration is disabled, the API returns 403 with message We are not currently accepting new registrations. The SPA /register route redirects to /registration-closed.

Password reset

StepTenantCentral
RequestPOST /api/tenant/v1/auth/forgot-passwordPOST /api/central/v1/auth/forgot-password
Email link{FRONTEND_URL}/reset-password/{token}?email={FRONTEND_URL}/central/reset-password/{token}?email=
ResetPOST /api/tenant/v1/auth/reset-passwordPOST /api/central/v1/auth/reset-password

Both reset endpoints use App\Rules\PasswordRule, which reads Central settings (password_min_length, password_require_special) plus always-on complexity rules.

Set FRONTEND_URL in the backend .env so reset/invite emails open the SPA.

Email verification

Tenant User and Central users implement MustVerifyEmail. Verification is required before protected Central and tenant application APIs can be used. Routes:

  • GET /api/tenant/v1/auth/email/verify/{id}/{hash} (signed)
  • POST /api/tenant/v1/me/email/verification-notification (self-service resend)
  • POST /api/tenant/v1/users/{user}/resend-verification (users.verify; throttled 6,1)
  • POST /api/tenant/v1/users/{user}/verify-email (users.verify; manual mark verified)

Admin verify/resend cannot target self or (for non-owners) the workspace owner. Manual verify and resend write platform audit events tenant_user_email_verified / tenant_user_verification_resent.

Impersonation compatibility

Login and registration issue tokens through TenantAuthBootstrapService::issueAccessToken() only — that service has no authorization side effects.
ImpersonationService reuses the same token helper for Central → Tenant handoff when issuing a tenant token.

Token ↔ workspace binding

Sanctum tenant tokens are not intrinsically bound to a workspace. Authenticated tenant routes use middleware tenant.user (EnsureTenantUserBelongsToCurrentTenant) so a token issued for workspace A is rejected (401) when X-Tenant-Domain / host resolves to workspace B.

InitializeTenancy re-resolves the tenant on every request and switches context when the domain/header changes (important for SPA clients that keep one API host and swap X-Tenant-Domain).

The SPA does not persist a workspace identifier in localStorage. It derives workspace context from the current host or the explicitly supplied login/request workspace, preventing stale browser state from selecting a different workspace.

Rate limiting

  • Login: throttle:auth-login (5/minute by email or IP)
  • Forgot/reset password: throttle:6,1
  • Register workspace: throttle:10,1

Tenant dashboard

GET /api/tenant/v1/dashboard (auth + verified + subscription + dashboard.view) returns welcome copy, workspace info, installed modules, a widget registry (module + permission + assignee scoped), and overall scope. See tenant-v1-dashboard.md.

Official documentation for the SaleOS SaaS Platform.