graphql-hive / graphql-hive/console
Allow a verified OIDC domain to be shared across multiple organizations (single-tenant / self-hosted deployments)
- Dominant language
- TypeScript
- Stars
- 483
- Forks
- 145
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 65
Description
## Summary
Hive enforces that a verified OIDC email domain can belong to **only one organization** in the
entire instance. At the same time, both **SCIM user provisioning** and **OIDC sign-in** require the
user's email domain to be verified **for that specific organization**. These two rules are in direct
tension for **self-hosted, single-tenant** deployments where one company runs Hive and structures
many organizations by line of business, but **every user shares one corporate email domain** (e.g.
`@company.com`).
The result: only the **first** organization to verify `company.com` can provision or admit
`@company.com` users. Every other organization is permanently blocked.
## Current behavior (all in `main`)
**A domain can be verified by only one org (instance-wide uniqueness):**
- DB unique index — `packages/migrations/src/actions/2026.02.25T00-00-00.oidc-integration-domains.ts`
```sql
CREATE UNIQUE INDEX IF NOT EXISTS "only_one_verified_domain_name_idx"
ON "oidc_integration_domains" ("domain_name")
WHERE "verified_at" IS NOT NULL;
```
- Register-time rejection — `packages/services/api/src/modules/oidc-integrations/providers/oidc-integrations.provider.ts`, `registerDomain()`
```ts
const existingVerifiedDomain = await this.oidcIntegrationStore.findVerifiedDomainByName(fqdnResult.data);
if (existingVerifiedDomain) {
return { type: 'error', message: 'This domain has already been verified with another organization.' };
}
```
- Verify-time guard — `packages/services/api/src/modules/oidc-integrations/providers/oidc-integration.store.ts`, `updateDomainVerifiedAt()`
```ts
// The NOT EXISTS statement is to avoid verifying the domain twice for two different otganizations
// only one org can own a domain
... AND NOT EXISTS (
SELECT 1 FROM "oidc_integration_domains" "x"
WHERE "x"."domain_name" = "oidc_integration_domains".domain_name AND "x"."verified_at" IS NOT NULL
)
```
**But per-org features require the domain to be verified for that org:**
- SCIM user creation — `packages/services/server/src/scim.ts`, `handleEmailValidation()`
```ts
const verifiedDomain = await oidcIntegrations.findVerifiedDomainByOIDCIntegrationIdAndDomainName(
oidcIntegrationId, emailDomainName, pool);
if (!verifiedDomain) {
return createSCIMError({ status: 400,
detail: 'Primary email address domain ownership is not verified for this organization.' });
}
```
- OIDC sign-in — `packages/services/api/src/modules/auth/lib/supertokens-strategy.ts` uses the same
per-org `findVerifiedDomainByOIDCIntegrationIdAndDomainName(oidcIntegrationId, domainName)`.
Because these consumers are **per-org** (`...ByOIDCIntegrationIdAndDomainName`) while verification is
**globally unique**, a shared corporate domain can only ever be verified — and therefore only ever
be usable — in a single organization.
## Steps to reproduce
1. Enable OIDC + SCIM for two organizations, `org-a` and `org-b`, in one self-hosted instance.
2. In `org-a`, register and verify `company.com`. ✅
3. In `org-b`, attempt to register `company.com` → `registerOIDCDomain` returns
*"This domain has already been verified with another organization."* ❌
4. SCIM `POST /Users` into `org-b` with a `@company.com` email → `400`,
*"Primary email address domain ownership is not verified for this organization."* ❌
## Proposal
Add an **opt-in, instance-level configuration** (env var) listing operator-trusted email domains.
Default is empty → **no behavior change** (per-org uniqueness preserved for Cloud). When a domain is
listed:
- SCIM email validation (`handleEmailValidation`) and OIDC sign-in treat the domain as verified for
**any** organization, without requiring a per-org `oidc_integration_domains` row.
- This leaves the strict per-org verification path, the register/verify guards, and the
`only_one_verified_domain_name_idx` index **completely intact** — no schema change. Trusted
domains simply short-circuit the verified-domain lookup at the consumer.
Sketch:
```ts
// consumer check becomes:
if (isTrustedEmailDomain(email, env.trustedEmailDomains)) return ok;
// else fall back to the existing per-org findVerifiedDomainByOIDCIntegrationIdAndDomainName(...)
```
This is the least invasive option: it doesn't relax the DB uniqueness constraint (so nothing about
Cloud's guarantees changes) and reframes verification from "per-org domain ownership" to
"instance-level operator-declared trust," which matches the single-tenant reality that the *operator*
(not any one org) owns the domain.
## Security considerations
Instance-wide trust is only safe when all organizations share one trust boundary — precisely the
single-tenant/self-hosted case. Cloud keeps the strict, secure default because the config is empty
unless an operator explicitly opts in.
## Alternatives considered
1. **Relax the uniqueness to per-(org, domain).** Change `only_one_verified_domain_name_idx` to
`(domain_name, oidc_integration_id)` and drop the register/verify guards, so each org verifies the
shared domain independently. Works, but requires a schema migration and forces redundant
(identical) DNS proofs of the same company domain across every org.
## References (upstream `main`)
- `packages/migrations/src/actions/2026.02.25T00-00-00.oidc-integration-domains.ts` — `only_one_verified_domain_name_idx`
- `packages/services/api/src/modules/oidc-integrations/providers/oidc-integrations.provider.ts` — `registerDomain()`
- `packages/services/api/src/modules/oidc-integrations/providers/oidc-integration.store.ts` — `updateDomainVerifiedAt()`, `findVerifiedDomainByName()`, `findVerifiedDomainByOIDCIntegrationIdAndDomainName()`
- `packages/services/server/src/scim.ts` — `handleEmailValidation()`
- `packages/services/api/src/modules/auth/lib/supertokens-strategy.ts` — OIDC sign-in domain check
Contributor guide
Research direction
Start with handleEmailValidation() in packages/services/server/src/scim.ts and the domain check in packages/services/api/src/modules/auth/lib/supertokens-strategy.ts, then trace how environment configuration is exposed. Preserve the existing per-organization lookup when the trusted-domain setting is empty, while configured domains pass both SCIM and OIDC checks across organizations without changing registration, verification, or the database index.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authentication, backend-api-design
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 64/100