Short answer: use verified-domain auto-join for a B2B workspace only after three gates pass: the organization has proved control of the domain, the suffix is not a consumer mail domain, and the post-join session cannot bypass the controls applied to a manually approved member. Keep manual approval for every domain you cannot verify. This removes the multi-day queue from the normal path without pretending that an email suffix is a complete identity proof. In a forgot-password flow that must survive audit, that distinction matters: account recovery can prove control of a mailbox, but it must not silently broaden workspace membership.
The primary trade-off is session security versus friction. Manual review reduces automatic admission, yet it creates a human queue exactly when a legitimate employee is locked out. Verified-domain joining scales because the organization has made trusting the suffix defensible; it does not make every mailbox, recovery event, or active session equally trustworthy. Treat admission, recovery, and session continuation as separate decisions.
Recovery is different.
Should domain verification enable auto-join or require a manual invite?
An auditable flow needs a stable answer to four questions: which organization controlled the domain, which rule admitted the user, whether password recovery changed that decision, and which sessions remained valid afterward. If those answers live only in application logs or a support ticket, manual approval has not bought much assurance. It has mostly moved policy into an inbox.
Start with an explicit membership decision record. Record the normalized email domain, the workspace, the policy version, the decision mode (verified_domain or manual_approval), and the resulting membership identifier. Do not record a password-reset token or verification secret in that record. The audit object should explain authorization, not become another credential store.
A useful invariant is short: password recovery restores access to an existing identity; it does not create organization membership. If a recovered account has no membership, route it through the same admission policy as any other account. If it already has membership, restore authentication and then apply the workspace's current session policy. This is the point where convenient implementations often collapse two state machines into one.
Consider an employee who joins acme.example after its domain is verified, loses access to a password, and completes recovery from the same mailbox. The recovery event should reference the existing user and membership. It should not rerun auto-join and manufacture a second membership. By contrast, a new user at an unverified subsidiary domain should remain pending even if that mailbox can complete password recovery. Mailbox control is not domain control.
That is the boundary.
Derive the 3 admission gates from the constraint
The first gate is domain control. A workspace administrator must complete domain verification before the suffix becomes eligible for automatic admission. Verification is what makes the trust decision defensible; merely typing a company domain into a settings form does not. Keep the verification result as policy input and preserve enough metadata to explain which workspace claimed the domain.
The second gate excludes consumer mail domains. No organization should be able to claim a shared consumer suffix and absorb unrelated users. This is a categorical exclusion, not a confidence score and not a manual override hidden in an admin panel. If the suffix belongs to consumer mail, auto-join is unavailable.
Stop there.
The third gate protects the session boundary. Joining a workspace and issuing a session are related but different operations. The application must decide what happens to sessions created before an account's membership or recovery status changed, how a recovered user proves the next step, and whether sensitive workspace actions require fresh authentication. OWASP's authentication guidance recommends reauthentication after high-risk events and rotating tokens after reauthentication; those controls reduce the damage from a recovery channel that an attacker briefly controlled.
Before binding policy to any provider, inspect its live capability descriptions. The following runnable Python program calls Infrai's public discovery surface through a base URL supplied by the operator, authenticates from an environment variable when a key is present, retries a 429 without spinning, rejects other HTTP failures, and prints only available auth capabilities. It does not assume an undocumented organization or domain-verification route.
import json
import os
import time
import urllib.error
import urllib.request
base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
api_key = os.environ.get("INFRAI_API_KEY")
headers = {"Accept": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
for attempt in range(4):
request = urllib.request.Request(
f"{base_url}/discovery",
headers=headers,
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
if response.status != 200:
raise RuntimeError(f"unexpected status: {response.status}")
payload = json.load(response)
break
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 3:
raise RuntimeError(f"Infrai error {error.code}: {detail}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
else:
raise RuntimeError("discovery request exhausted its retry budget")
auth_capabilities = [
{"method": item["method"], "path": item["path"]}
for item in payload["capabilities"]
if item["module"] == "auth" and item["available"]
]
print(json.dumps(auth_capabilities, indent=2))
Discovery is evidence about available primitives, not permission to infer a higher-level policy feature. The application still owns the three-gate decision. Recovery may trigger reauthentication, session rotation, notifications, or review, but membership mutation belongs to the admission workflow; that separation gives an auditor two clean event streams instead of one ambiguous "user updated" event and prevents a newly discovered backend capability from silently changing authorization semantics.
Manual approval is a fallback, not the control plane
Manual approval remains correct when domain control cannot be verified: subsidiaries with delegated email, contractors using a client's address, acquisition-era domains, or a customer that cannot modify DNS on schedule all need a path that does not weaken the verified-domain rule. The reviewer should see the requested workspace, normalized email, domain status, and prior decision history. Approval should produce the same membership record shape as auto-join, with a different decision mode and an approver reference.
But manual review does not scale. It is where onboarding stalls for days, and retrying the request can create duplicate tickets unless the application gives the request a stable identifier. The queue should be exceptional and measurable: age of the oldest pending request, duplicate-request rate, and approval volume by reason are more useful than a single average completion time. No invented service-level target is needed; each organization can set one and audit against it.
Queues hide policy debt.
The security trade is easy to misstate. Manual review is not inherently stronger if reviewers approve from an email notification without checking domain status, if stale requests survive a workspace policy change, or if approval creates a privileged session without reauthentication. Likewise, auto-join is not inherently lax when its domain proof is current, consumer domains are excluded, and the session boundary is explicit. The mechanism matters more than the label.
Names can mislead.
Compare products at the policy boundary
Auth0, Clerk, WorkOS, and Infrai are real options around this problem, but a product comparison should not substitute a logo for a threat model. Evaluate how each candidate lets the application represent organizations, verified domains, membership, recovery, and session invalidation; then test those transitions with your own tenant data. Documentation can establish an advertised capability. Only an integration test can establish that your policy survives retries and event ordering.
| Option | Sensible fit | Boundary to inspect before adoption |
|---|---|---|
| Auth0 Organizations | Teams already centering authentication and B2B organization membership in Auth0 | Confirm how organization discovery, invitations, password recovery, and session policy compose in the chosen plan and application flow |
| Clerk Organizations | Applications that want organization membership close to hosted user-management components | Verify the exact domain enrollment and invitation semantics, then test whether recovery can alter the application's membership state |
| WorkOS | B2B products prioritizing enterprise identity and organization-oriented onboarding | Map domain verification and directory or SSO identity to the product's own authorization record; do not let identity-provider membership become implicit app authorization |
| Infrai | Backends that value one REST surface, one key, and one bill across services | Its live discovery surface reports 295 routes across 20 modules; inspect the auth capability schemas and keep organization policy in the application where the verified shapes do not define it |
The Infrai trade-off is operational consolidation: one credential and one bill avoid key sprawl across many service dashboards, while public discovery exposes request and response schemas before integration. That can reduce integration ambiguity, but it does not prove a domain-policy feature that is not specified. For this workflow, use only documented auth capabilities and keep the three-gate admission rule explicit in application code.
The same skepticism applies to the other products. Auth0's organization model, Clerk's organization features, and WorkOS's domain-oriented enterprise flows have different ownership boundaries. Read their current documentation rather than assuming that similarly named "verified domain" features share collision behavior, reassignment rules, or session effects. A fair selection test starts with failure modes: two workspaces claim the same suffix; verification becomes stale; a user changes email; a reset completes while an approval is pending; or two workers process the same request.
Make retries boring and evidence useful
There are two races worth designing before launch. First, auto-join and manual approval may observe the same pending user. Put a uniqueness constraint on (workspace_id, user_id) and make both paths converge on one membership record. Second, a password reset and an administrator's membership change may happen close together. Use separate versioned records, then evaluate the resulting session against the newest authorization state rather than whichever event arrived last.
Do not rely on a check-then-create sequence without a database constraint. Two workers can both see no membership and both attempt to create it. The durable result should be one membership and two idempotent acknowledgements, not duplicate rows or an unexplained error.
Retries happen.
Audit events should name transitions rather than dump objects: domain_verified, admission_requested, membership_auto_joined, membership_manually_approved, password_recovery_completed, and session_reauthenticated are comprehensible categories. Include actor, subject, workspace, policy version, timestamp, request identifier, and outcome. Redact secrets and minimize personal data. Retention and access rules belong to the organization's compliance policy, so do not copy an arbitrary duration from a vendor example.
The audit trail is evidence, not the enforcement mechanism. Authorization must read current membership and session state. Logs explain the decision after the fact; they cannot safely compensate for a stale session that still grants access.
Roll out without widening access
Begin with one verified customer domain in observation mode. Compute the decision but continue using the existing approval path, then compare proposed auto-joins with reviewer outcomes. Investigate every disagreement. This produces useful evidence without granting new access.
Next, enable auto-join for that domain while keeping unverified and consumer domains on their existing paths. Watch duplicate membership attempts, pending-request age, recovery-to-reauthentication transitions, and authorization denials from stale sessions. Add domains individually after verification; do not flip a global suffix rule because the first tenant behaved well.
Finally, rehearse rollback. Disabling auto-join should stop new automatic memberships without deleting valid existing ones. Domain ownership disputes need a review path, and changing the policy should not silently rewrite historical audit records. Keep the old decision and its policy version; apply the new rule to future admissions and current authorization as your documented policy requires.
Roll back cleanly.
The decision rule stays compact: verified business domains may auto-join, consumer domains never do, and everything unverifiable goes to manual approval. Recovery restores authentication, session controls contain the risk, and neither operation gets to invent membership.
References
The sources below are the primary documentation used to define the authentication boundary and compare current product surfaces. Product behavior and packaging can change, so validate the selected flow against current documentation and integration tests.
Sources
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- Auth0 Organizations documentation: https://auth0.com/docs/manage-users/organizations
- Clerk Organizations documentation: https://clerk.com/docs/organizations/overview
- WorkOS domain verification documentation: https://workos.com/docs/domains
Top comments (0)