Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

17 KiB

Scratchpad — MSP Domain-Scoped SSO Discovery

  • Plan slug: 2026-02-24-msp-domain-scoped-sso-discovery
  • Created: 2026-02-24

What This Is

Working notes for shifting MSP SSO provider enablement from user-based pre-auth checks to domain-based tenant discovery, while preserving anti-enumeration posture.

Decisions

  • (2026-02-24) Do not ship per-user provider enablement on public login because it introduces user-enumeration risk.
  • (2026-02-24) Use domain-level tenant discovery for MSP login provider filtering.
  • (2026-02-24) Keep /auth/msp/signin and existing email links unchanged (no hostname migration requirement in this phase).
  • (2026-02-24) Keep client portal out of scope.
  • (2026-02-24) Keep unknown-user behavior non-reactive/generic in resolver responses.

Discoveries / Constraints

  • (2026-02-24) Existing MSP SSO buttons are currently email-gated only, then call /api/auth/msp/sso/resolve.
  • (2026-02-24) Existing resolver logic in packages/auth/src/lib/sso/mspSsoResolution.ts currently performs user lookup for source selection.
  • (2026-02-24) Prior implementation plan exists at ee/docs/plans/2026-02-23-msp-tenant-first-sso-provider-resolution and should be treated as superseded for pre-auth provider enablement strategy.
  • (2026-02-24) Domain-level discovery can still reveal tenant/provider posture for a domain; this is acceptable for this phase, while user-existence signals remain prohibited.
  • (2026-02-24) Added server/migrations/20260224103000_create_msp_sso_tenant_login_domains.cjs with table msp_sso_tenant_login_domains (tenant, id, domain, is_active, audit actor/timestamps), plus backfill from tenants.email only for globally-unambiguous domains.
  • (2026-02-24) Cross-tenant duplicate domains are intentionally allowed in persistence; uniqueness is enforced only per-tenant via (tenant, lower(domain)) so runtime discovery can fail-closed on ambiguity.
  • (2026-02-24) Added domain-management indexes: msp_sso_tenant_login_domains_domain_active_idx, msp_sso_tenant_login_domains_tenant_active_idx, and unique tenant-local msp_sso_tenant_login_domains_tenant_domain_uniq.
  • (2026-02-24) Added tenant settings actions in packages/integrations/src/actions/integrations/mspSsoDomainActions.ts; listMspSsoLoginDomains enforces internal admin auth and returns normalized active domains for the current tenant.
  • (2026-02-24) saveMspSsoLoginDomains now supports create/update/remove via desired-state writes: inserts new domains, re-activates existing matches, and deactivates removed rows.
  • (2026-02-24) Domain save path normalizes inputs (Unicode cleanup, trim, lowercase, optional leading @ strip) and rejects malformed domains with deterministic validation errors.
  • (2026-02-24) Domain write-time conflict policy: reject saves when any desired active domain is already active for another tenant; response includes neutral error + conflicting domain list for admin remediation.
  • (2026-02-24) Added Providers UI section MspSsoLoginDomainsSettings and wired it into IntegrationsSettingsPage providers tab below Google/Microsoft credential cards.
  • (2026-02-24) Providers UI supports full domain list editing: add-new input, per-row inline edits, per-row remove controls, and explicit save/refresh actions.
  • (2026-02-24) Providers UI renders save failures in neutral actionable alerts (validation messages + conflict domain hints) without exposing backend internals.
  • (2026-02-24) Added discovery endpoint POST /api/auth/msp/sso/discover with invariant response shape { ok: true, providers: [] } and signed discovery cookie issuance on valid requests.
  • (2026-02-24) Discovery route normalizes/validates email before lookup and derives a normalized domain via shared helper (extractDomainFromEmail).
  • (2026-02-24) Discovery route uses per-IP/per-email-hash memory rate limiting and returns the same neutral { ok: true, providers: [] } payload on limit hits.
  • (2026-02-24) Tenant discovery now resolves only from msp_sso_tenant_login_domains by domain and does not query full email/user records.
  • (2026-02-24) Domain lookup treats multi-tenant matches as ambiguous/unresolved and falls back to app-level provider evaluation (fail-closed for tenant context).
  • (2026-02-24) Tenant-scoped discovery computes Google readiness via tenant secrets (google_client_id + google_client_secret).
  • (2026-02-24) Tenant-scoped discovery computes Microsoft readiness via tenant secrets (microsoft_client_id + microsoft_client_secret).
  • (2026-02-24) Unresolved-domain discovery falls back to app-level provider readiness via GOOGLE_OAUTH_* and MICROSOFT_OAUTH_* app secrets/env.
  • (2026-02-24) Discovery endpoint response is invariant across invalid/limited/error paths and only returns allowed provider IDs (google, azure-ad) when available.
  • (2026-02-24) Added signed discovery cookie helper (createSignedMspSsoDiscoveryCookie / parse verifier) with only source/tenant/providers/timing metadata; no OAuth client IDs or secrets.
  • (2026-02-24) Discovery endpoint now rotates discovery cookie on valid requests and clears stale/invalid context cookies with maxAge: 0 on neutral failure responses.
  • (2026-02-24) MSP SsoProviderButtons now calls /api/auth/msp/sso/discover whenever a syntactically valid email is entered and uses response providers as the client-side allow-list input.
  • (2026-02-24) MSP SSO buttons remain disabled for invalid email and during discovery fetch in-flight state; enablement happens only after discovery completes.
  • (2026-02-24) Provider buttons now honor discovery allow-list strictly: unsupported providers stay disabled and disabled clicks do not invoke resolver/start.
  • (2026-02-24) Added local remembered-provider UX (localStorage key msp_sso_last_provider) and only marks preferred provider when it is still in the discovered eligible set.
  • (2026-02-24) Resolver now consumes signed discovery context cookie and passes parsed discovery metadata into source resolution before issuing msp_sso_resolution.
  • (2026-02-24) Resolver/source helper enforces discovered provider allow-list; provider attempts outside cookie-allowed set fail with generic response.
  • (2026-02-24) Resolver fallback behavior: when discovery cookie is missing/invalid/expired, source resolution uses app-level provider readiness only.
  • (2026-02-24) Resolver maintains one external generic failure schema/message for invalid payload, limit hits, disallowed provider, missing credentials, and internal errors.
  • (2026-02-24) OAuth callback/user mapping path was left unchanged; discovery/resolver changes only affect pre-auth provider eligibility and source selection.
  • (2026-02-24) MSP credentials form submit path in MspLoginForm is unchanged; SSO discovery logic is isolated to SsoProviderButtons.
  • (2026-02-24) Client portal login remains unchanged: client SSO affordance stays commented/disabled and does not invoke MSP discovery logic.
  • (2026-02-24) Updated provider setup docs with explicit ordering: configure provider credentials, then configure tenant MSP login domains before relying on MSP SSO.
  • (2026-02-24) Added env/docs guidance clarifying unresolved-domain behavior: discovery falls back to app-level GOOGLE_OAUTH_* / MICROSOFT_OAUTH_* provider configuration.
  • (2026-02-24) CE/EE parity enforced for MSP SSO button behavior with a shared discovery/resolve contract test (ssoProviderButtons.ceEeParity.test.ts).
  • (2026-02-24) Added route/callback contract coverage proving /auth/msp/signin links and callbackUrl passthrough/default behavior remain unchanged.

Commands / Runbooks

  • (2026-02-24) Scaffoled this plan:
    • python3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/scaffold_plan.py "MSP Domain-Scoped SSO Discovery" --slug msp-domain-scoped-sso-discovery
  • (2026-02-24) Validate this plan bundle:
    • python3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/validate_plan.py ee/docs/plans/2026-02-24-msp-domain-scoped-sso-discovery
  • (2026-02-24) Validate migration contract test:
    • cd server && npx vitest run src/test/unit/migrations/mspSsoTenantLoginDomainsMigration.test.ts
  • (2026-02-24) Validate domain actions unit tests:
    • cd server && npx vitest run --coverage.enabled=false ../packages/integrations/src/actions/integrations/mspSsoDomainActions.test.ts
  • (2026-02-24) Validate discovery endpoint tests:
    • cd server && npx vitest run --coverage.enabled=false src/app/api/auth/msp/sso/discover/route.test.ts
  • (2026-02-24) Validate SSO discovery helper tests:
    • cd server && npx vitest run --coverage.enabled=false ../packages/auth/src/lib/sso/mspSsoResolution.test.ts
  • (2026-02-24) Validate MSP discovery + resolver + SSO button tests:
    • cd server && npx vitest run --coverage.enabled=false src/app/api/auth/msp/sso/discover/route.test.ts src/app/api/auth/msp/sso/resolve/route.test.ts ../packages/auth/src/lib/sso/mspSsoResolution.test.ts ../packages/auth/src/components/SsoProviderButtons.msp.test.tsx
  • (2026-02-24) Validate MSP sign-in route and callback contracts:
    • cd server && npx vitest run --coverage.enabled=false ../packages/auth/src/components/MspSignInRoute.contract.test.ts

Gotchas

  • (2026-02-24) vitest default coverage output can fail with ENOSPC in this worktree; use --coverage.enabled=false for targeted unit runs while iterating.
  • Previous plan (superseded approach):
    • ee/docs/plans/2026-02-23-msp-tenant-first-sso-provider-resolution/PRD.md
  • MSP login + SSO buttons:
    • packages/auth/src/components/MspLoginForm.tsx
    • packages/auth/src/components/SsoProviderButtons.tsx
  • Current resolver endpoint:
    • server/src/app/api/auth/msp/sso/resolve/route.ts
  • Discovery endpoint:
    • server/src/app/api/auth/msp/sso/discover/route.ts
  • Resolver helper library:
    • packages/auth/src/lib/sso/mspSsoResolution.ts
  • Provider readiness/actions:
    • packages/integrations/src/actions/integrations/providerReadiness.ts

Open Questions

  • Should domain conflicts be hard-blocked at write-time or tolerated and treated as unresolved at read-time?
  • Should unresolved-domain app-fallback provider exposure be configurable by environment?
  • Should remembered provider preference be localStorage only or include signed cookie metadata?
  • (2026-02-24) Completed T001: Migration creates tenant MSP SSO login-domain persistence model with expected columns.
  • (2026-02-24) Completed T002: Migration rollback removes tenant MSP SSO login-domain persistence objects cleanly.
  • (2026-02-24) Completed T003: Schema includes indexes supporting fast lookup by normalized domain and tenant domain listing.
  • (2026-02-24) Completed T004: List login-domain action denies unauthorized users and client users.
  • (2026-02-24) Completed T005: List login-domain action returns normalized, deduplicated tenant domains.
  • (2026-02-24) Completed T006: Save login-domain action persists valid domains for the tenant.
  • (2026-02-24) Completed T007: Save login-domain action lowercases and trims domains before persistence.
  • (2026-02-24) Completed T008: Save login-domain action rejects malformed domains with a deterministic validation error.
  • (2026-02-24) Completed T009: Save login-domain action prevents duplicate domains in one tenant payload.
  • (2026-02-24) Completed T010: Cross-tenant domain conflict behavior follows configured policy (reject or mark ambiguous).
  • (2026-02-24) Completed T011: Removing/deactivating a tenant login domain updates subsequent listing and discovery reads.
  • (2026-02-24) Completed T012: Providers settings page renders MSP SSO login-domain management section.
  • (2026-02-24) Completed T013: Providers UI add-domain flow invokes save action and refreshes rendered domain list.
  • (2026-02-24) Completed T014: Providers UI remove-domain flow invokes save action and removes domain row from view.
  • (2026-02-24) Completed T015: Providers UI shows malformed-domain validation errors without exposing backend internals.
  • (2026-02-24) Completed T016: Providers UI shows conflict/ambiguity error state with neutral language.
  • (2026-02-24) Completed T017: Discovery endpoint returns { ok: true, providers: [] } for invalid email input.
  • (2026-02-24) Completed T018: Discovery endpoint normalizes email and extracts domain correctly from mixed-case input.
  • (2026-02-24) Completed T019: Discovery endpoint rate-limited calls return the same neutral response schema.
  • (2026-02-24) Completed T020: Known mapped domain with tenant Microsoft configured returns only azure-ad.
  • (2026-02-24) Completed T021: Known mapped domain with both tenant providers configured returns google and azure-ad.
  • (2026-02-24) Completed T022: Known mapped domain with no tenant providers configured returns empty providers list.
  • (2026-02-24) Completed T023: Unresolved domain with app Google fallback configured returns only google.
  • (2026-02-24) Completed T024: Unresolved domain with app Microsoft fallback configured returns only azure-ad.
  • (2026-02-24) Completed T025: Unresolved domain with no app fallback providers configured returns empty provider list.
  • (2026-02-24) Completed T026: Discovery implementation contract does not branch on specific-user existence lookup results.
  • (2026-02-24) Completed T027: Discovery logs avoid raw email and include only safe domain/hash metadata.
  • (2026-02-24) Completed T028: Discovery context cookie is signed and excludes OAuth client IDs/secrets.
  • (2026-02-24) Completed T029: Discovery context cookie expires according to configured short TTL.
  • (2026-02-24) Completed T030: Discovery endpoint rotates cookie on valid requests and clears stale context on invalid input.
  • (2026-02-24) Completed T031: MSP SSO buttons remain disabled for invalid/empty email input.
  • (2026-02-24) Completed T032: MSP SSO buttons remain disabled while discovery request is in flight.
  • (2026-02-24) Completed T033: MSP login enables only Microsoft button when discovery returns azure-ad only.
  • (2026-02-24) Completed T034: MSP login enables both buttons when discovery returns both providers.
  • (2026-02-24) Completed T035: MSP login keeps unsupported provider buttons disabled based on discovery response.
  • (2026-02-24) Completed T036: Last-selected provider preference is persisted locally when user completes provider click.
  • (2026-02-24) Completed T037: Remembered provider is only auto-selected when it is still present in discovered provider list.
  • (2026-02-24) Completed T038: Clicking a disabled provider button never triggers resolver/start API call.
  • (2026-02-24) Completed T039: Resolver consumes valid discovery cookie and uses tenant/source metadata for provider start.
  • (2026-02-24) Completed T040: Resolver rejects provider attempts not included in discovered allowed provider set using generic failure response.
  • (2026-02-24) Completed T041: Resolver falls back to app-level behavior when discovery cookie is missing, invalid, or expired.
  • (2026-02-24) Completed T042: Unknown-user and known-user paths remain externally indistinguishable in resolver responses.
  • (2026-02-24) Completed T043: Resolver rate-limit failures preserve the same generic response shape and wording.
  • (2026-02-24) Completed T044: Resolver logging excludes raw email and other sensitive identifiers.
  • (2026-02-24) Completed T045: OAuth callback flow for unknown users remains unchanged (no discovery-specific account-existence messaging).
  • (2026-02-24) Completed T046: MSP credentials sign-in flow remains functional and independent from SSO discovery outcome.
  • (2026-02-24) Completed T047: Client portal sign-in flow remains unchanged with no MSP discovery behavior bleed-through.
  • (2026-02-24) Completed T048: CE/EE SSO component wiring continues to route MSP login through shared discovery-enabled SSO entrypoint.
  • (2026-02-24) Completed T049: DB-backed integration happy path: mapped tenant domain + tenant Microsoft secrets yields discovery providers ["azure-ad"].
  • (2026-02-24) Completed T050: DB-backed integration guard path: ambiguous duplicate domain mapping resolves as unresolved and returns neutral provider set.
  • (2026-02-24) Completed T051: DB-backed integration guard path: inactive/deleted domain mappings are ignored by discovery.
  • (2026-02-24) Completed T052: Documentation contract includes tenant login-domain setup in provider configuration instructions.
  • (2026-02-24) Completed T053: Environment/docs contract explains unresolved-domain app-fallback provider behavior.
  • (2026-02-24) Completed T054: Route contract verifies /auth/msp/signin path remains unchanged after discovery rollout.
  • (2026-02-24) Completed T055: Callback URL passthrough remains intact for MSP login redirects when SSO discovery is active.
  • (2026-02-24) Completed T056: Backfill migration populates initial login-domain entries from tenant primary email domain only when unambiguous.
  • (2026-02-24) Completed T057: Backfill migration skips conflicting candidate domains and records deterministic no-op behavior.
  • (2026-02-24) Completed T058: CE and EE both expose discovery route + resolver gating behavior with identical external API contracts.