# 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.

## Links / References

- 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.
