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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
17 KiB
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/signinand 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.tscurrently 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-resolutionand 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.cjswith tablemsp_sso_tenant_login_domains(tenant,id,domain,is_active, audit actor/timestamps), plus backfill fromtenants.emailonly 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-localmsp_sso_tenant_login_domains_tenant_domain_uniq. - (2026-02-24) Added tenant settings actions in
packages/integrations/src/actions/integrations/mspSsoDomainActions.ts;listMspSsoLoginDomainsenforces internal admin auth and returns normalized active domains for the current tenant. - (2026-02-24)
saveMspSsoLoginDomainsnow 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
MspSsoLoginDomainsSettingsand wired it intoIntegrationsSettingsPageproviders 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/discoverwith 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_domainsby 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_*andMICROSOFT_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: 0on neutral failure responses. - (2026-02-24) MSP
SsoProviderButtonsnow calls/api/auth/msp/sso/discoverwhenever 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 (
localStoragekeymsp_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
MspLoginFormis unchanged; SSO discovery logic is isolated toSsoProviderButtons. - (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/signinlinks 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)
vitestdefault coverage output can fail withENOSPCin this worktree; use--coverage.enabled=falsefor 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.tsxpackages/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
googleandazure-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-adonly. - (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/signinpath 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.