Skip to content

필사 모드: Designing SSO for Multi-Tenant SaaS — An Architecture for Enterprise Customer Onboarding

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

Introduction — What "Do You Support SSO?" Really Means

If you run a B2B SaaS, there is one question you will inevitably hear during enterprise sales: "Do you support SSO?" This SSO is not "Sign in with Google" social login. What the customer is really asking is this:

"Can our employees log in to your service using **our company IdP** (Okta, Entra ID, our own Keycloak, and so on)?"

In other words, SSO in a multi-tenant SaaS means a **federation architecture where every tenant (customer company) connects a different external IdP**. It is a completely different class of design problem from wiring a single IdP for internal SSO. As of 2026, SSO (and SCIM) has effectively become a bid qualification item on enterprise security checklists, and amid the passkeys-by-default and Zero Trust waves, the principle that "the customer IdP owns authentication" keeps getting stronger.

This post draws the full picture of multi-tenant SSO: per-tenant IdP configuration models (realm-per-tenant vs Keycloak Organizations), home realm discovery, domain verification, JIT vs SCIM, self-service onboarding UI, and the SSO tax pricing debate.

A Bird's-Eye View of the Architecture

Let us first introduce the cast.

Customer A (acme.com) Our SaaS Customer B (globex.io)

+--------------------+ +---------------------+ +--------------------+

| Okta (IdP) | | Central auth layer | | Entra ID (IdP) |

| |<---->| (Keycloak etc.) |<---->| |

| SAML 2.0 | SAML | | OIDC | OIDC |

+--------------------+ | - per-tenant IdP | +--------------------+

| - domain -> IdP |

Customer C (startup) | routing |

+--------------------+ | - JIT / SCIM |

| No IdP |----->| - session mgmt |

| Email + passkeys | +----------+----------+

+--------------------+ |

v

+---------------------+

| SaaS application |

| (tenant-isolated |

| authorization) |

+---------------------+

There are three core design principles.

1. **The application never talks to IdPs directly.** The app speaks OIDC only to a central auth layer (your own Keycloak, or a service like Auth0/WorkOS), while SAML/OIDC federation with customer IdPs is handled by that layer. This is the broker pattern.

2. **Tenant identification comes before authentication.** Before asking "who are you," you must know "which company are you from" in order to route to the right IdP.

3. **Delegate authentication, never authorization.** The customer IdP vouches only for "this person is an employee of acme.com"; roles and permissions inside our service are ours to manage.

Per-Tenant IdP Configuration Models — realm-per-tenant vs Organizations

If you build the central auth layer on Keycloak, how to model tenants is the first fork in the road.

Model 1: realm-per-tenant

The traditional approach: one Keycloak realm per tenant.

Keycloak

├── realm: acme (Okta SAML federation, acme-only clients)

├── realm: globex (Entra OIDC federation, globex-only clients)

└── realm: startupz (local accounts + passkeys)

The advantage is perfect isolation: authentication flows, password policies, token lifetimes, even themes can differ freely per tenant. But the drawbacks are fatal.

- **Scalability**: a realm is a heavyweight object. At hundreds to thousands of tenants, startup time, cache memory, and admin console performance all degrade as the realm count grows. The Keycloak team itself does not recommend operating large numbers of realms.

- **Operational burden**: client registration, key rotation, and configuration changes must be repeated per realm. One hundred tenants means one hundred configuration changes (automation mandatory).

- **App integration complexity**: the app must handle a different issuer URL per tenant. Many OIDC libraries do not support multiple issuers gracefully.

Model 2: single realm + Keycloak Organizations

The [Organizations](https://www.keycloak.org/docs/latest/server_admin/index.html#_managing_organizations) feature, made fully supported in Keycloak 26, attacks this problem head on. It introduces a first-class tenant concept called "organization" inside a single realm.

Keycloak

└── realm: saas-prod

├── organization: acme

│ ├── domains: acme.com, acme.co.kr (verified)

│ ├── identity provider: acme-okta (SAML)

│ └── members: users belonging to acme

├── organization: globex

│ ├── domain: globex.io

│ ├── identity provider: globex-entra (OIDC)

│ └── members: users belonging to globex

└── organization: startupz

├── domain: startupz.dev

└── (no IdP — local authentication)

What Organizations gives you:

- **Per-organization IdP linkage**: attach a SAML/OIDC IdP to each organization and route users of that organization's domains to it.

- **Domain mapping**: register email domains on an organization, and the login screen can determine the organization from the email alone (a built-in implementation of home realm discovery).

- **Membership management**: user invitations, organization member attributes, and APIs to list members per organization.

- **Organization claims in tokens**: issued tokens include organization information as claims, so the app immediately knows the tenant context.

{

"iss": "https://auth.example-saas.com/realms/saas-prod",

"sub": "f3a8c2e1-9b47-4d6a-8c21-0e5f7a9b3d44",

"preferred_username": "jane@acme.com",

"organization": {

"acme": {

"id": "b1e6f0c2-3d8a-47e5-9f12-6c4b8a0d2e91"

}

},

"exp": 1781234567,

"aud": "saas-web"

}

Comparison and selection criteria

| Criterion | realm-per-tenant | Single realm + Organizations |

| --- | --- | --- |

| Isolation level | Highest (policies/keys/themes fully separate) | Logical isolation (policies shared at realm level) |

| Tenant count scalability | Hits limits at dozens | Realistic up to thousands of organizations |

| App integration | Per-tenant issuer — complex | Single issuer — simple |

| Per-tenant auth policy differentiation | Unlimited | Limited (auth flows are realm-scoped) |

| Operational automation burden | Heavy | Light |

| Best fit | Few large customers requiring hard isolation by regulation | Most typical B2B SaaS |

The 2026 conclusion is clear: **for a typical B2B SaaS, single realm + Organizations is the default**, and only exceptional customers requiring contractual hard isolation (finance, public sector) get a dedicated realm or instance — a hybrid approach. Thanks to zero-downtime rolling updates in Keycloak 26.6, the upgrade risk of operating one large realm is also far smaller than it used to be.

Home Realm Discovery — Email-Domain-Based IdP Routing

When a user lands on the login page, we do not yet know who they are. The process of deciding which IdP to send them to is **home realm discovery (HRD)**. The canonical implementation is the "email-first (identifier-first)" pattern.

1. User enters email: jane@acme.com

2. System extracts domain: acme.com

3. Look up domain -> organization -> IdP mapping

- acme.com belongs to organization acme; IdP is acme-okta (SAML)

4. Branch:

- IdP exists -> redirect with SAML AuthnRequest to acme-okta

- No IdP -> show password/passkey UI

5. IdP authentication completes -> callback -> session issued

With Keycloak Organizations this flow comes built in: when the user enters an email on the login page, Keycloak checks whether the domain maps to an organization and automatically redirects to the linked IdP. If you implement it yourself, it looks like this.

@PostMapping("/auth/discover")

public ResponseEntity<DiscoveryResponse> discover(@RequestBody DiscoveryRequest req) {

String domain = extractDomain(req.email()); // "acme.com"

Optional<TenantIdpConfig> idp = tenantService.findVerifiedIdpByDomain(domain);

if (idp.isPresent()) {

// Federated tenant: build the authorization URL (OIDC authorize or SAML SSO URL)

String redirect = authUrlBuilder.build(idp.get(), req.relayState());

return ResponseEntity.ok(DiscoveryResponse.federated(redirect));

}

// Non-federated tenant: proceed with local auth (password/passkeys)

return ResponseEntity.ok(DiscoveryResponse.local());

}

Things to watch out for in HRD design:

- **Do not leak tenant existence**: a response like "this domain is not registered" lets attackers enumerate your customer list. It is safer to show the identical next-step UI regardless of whether an IdP exists.

- **Personal domains**: public domains like gmail.com must be excluded from organization mapping.

- **Multiple IdPs per domain**: mergers and acquisitions can put multiple IdPs behind one domain. You need a fallback that shows an IdP chooser screen.

Tenant identification in SP-initiated flows

Beyond HRD, there are several channels for identifying the tenant.

| Method | Example | Trade-offs |

| --- | --- | --- |

| Email domain (identifier-first) | typing jane@acme.com | Universal; adds one UX step |

| Tenant-specific URL | acme.example-saas.com | Bookmark friendly; subdomain management needed |

| Tenant-specific login path | /login/acme | Simple to build; exposes tenant in shared URLs |

| IdP-initiated SSO | clicking the app tile in the customer portal | Customer friendly but less recommended for security (especially SAML unsolicited responses) |

In practice the most common combination is "subdomain + identifier-first fallback." Because IdP-initiated SAML has a CSRF-like attack surface, prefer forcing SP-initiated flows where possible and point customer portal tiles at the SP login URL.

Domain Verification — The Trust Foundation of Federation

The mapping between domains and organizations is the bedrock of security. If anyone could register any domain without verification, a malicious tenant could claim acme.com under its own organization and hijack acme employee logins into its own IdP.

The standard verification method is a DNS TXT record.

1. Tenant admin requests domain registration: acme.com

2. System issues a verification token:

saas-verify=4f8a2c1e-7b3d-4e9a-b6f0-1c5d8e2a9b47

3. Admin adds the TXT record to acme.com DNS

4. System confirms the token via DNS lookup -> verified

5. Periodic re-verification afterwards (detect loss of domain ownership)

The core of the verification side is simple

dig +short TXT acme.com | grep "saas-verify=4f8a2c1e-7b3d-4e9a-b6f0-1c5d8e2a9b47"

Additional principles:

- **Never activate routing before verification**: HRD routing for a domain must stay off until verification completes.

- **Handle domain conflicts**: refuse registration of a domain already verified by another organization, and provide a dispute process (manual review).

- **Re-verification cadence**: re-check periodically to detect domain expiry or sale.

JIT Provisioning vs SCIM

Authentication against the customer IdP works — but who creates the user record inside our service?

JIT (Just-in-Time) provisioning

Create the user on the fly at first login, based on the SAML assertion or OIDC claims.

public User jitProvision(OidcUserInfo info, Organization org) {

return userRepository.findByIssuerAndSubject(info.issuer(), info.subject())

.orElseGet(() -> {

User user = User.builder()

.email(info.email())

.displayName(info.name())

.organizationId(org.id())

.role(org.defaultRole()) // least privilege by default

.source(UserSource.FEDERATED_JIT)

.build();

auditLog.record("user.jit_provisioned", user);

return userRepository.save(user);

});

}

Comparison

| Criterion | JIT | SCIM |

| --- | --- | --- |

| Account creation time | At first login | Immediately on hire/assignment (pre-created) |

| Account deactivation | Impossible — users who never log in are unknown | IdP pushes instantly |

| Group/permission sync | Snapshot of claims at login time | Real-time on change |

| Implementation cost | Low | Requires a SCIM server |

| Pre-login collaboration features (invites, mentions) | Incomplete — users do not exist yet | Full roster pre-populated |

The conclusion is "both." The industry-standard pattern is **JIT as the baseline, SCIM as the enterprise-tier upgrade**. What matters most is the deprovisioning angle: for JIT-only tenants, be explicit with the customer security team that departed employees' accounts survive on our side, and compensate with shorter session lifetimes and re-authentication intervals.

Designing a Self-Service SSO Onboarding UI

Once you have dozens of enterprise customers, handling SSO setup through support tickets goes bankrupt. The answer is a self-service wizard that tenant admins drive themselves.

[Step 1] Choose protocol

SAML 2.0 | OIDC

|

[Step 2] Present our side's details (in copyable form)

SAML: SP Entity ID, ACS URL, SP metadata XML download

OIDC: Redirect URI, recommended scopes

|

[Step 3] Enter customer IdP details

SAML: IdP metadata URL or XML upload (incl. signing certificate)

OIDC: issuer URL (discovery auto-fetched), client_id, client_secret

|

[Step 4] Attribute mapping

email, name, group claim mapping (dropdowns + sensible defaults)

|

[Step 5] Test login

Before activation, the admin performs a test authentication

On failure, render the SAML response/error in human-readable form

|

[Step 6] Choose activation policy

- SSO optional (local login still allowed)

- SSO enforced (local login blocked, except break-glass admins)

The design details decide success or failure.

- **Automatic metadata parsing**: when SAML metadata XML or an OIDC discovery document is uploaded/entered, auto-extract endpoints and certificates. Every additional manual field translates into more misconfiguration tickets.

- **Mandatory test login**: simply blocking activation until the test passes prevents the classic "we turned on SSO and now nobody can log in" incident.

- **Break-glass accounts**: even in SSO-enforced mode, at least one tenant admin must be able to log in with local authentication (strong MFA required). Without an unlock path for IdP outages or certificate expiry, support hell awaits.

- **Certificate expiry monitoring**: automatically notify tenant admins 30 and 7 days before SAML signing certificates expire. Certificate expiry is the number-one cause of SAML integration outages.

If you build on Keycloak, the wizard backend is implemented with Admin API calls.

Example: attach an OIDC IdP for an organization (Admin API)

curl -X POST "https://auth.example-saas.com/admin/realms/saas-prod/identity-provider/instances" \

-H "Authorization: Bearer $ADMIN_TOKEN" \

-H "Content-Type: application/json" \

-d '{

"alias": "globex-entra",

"providerId": "oidc",

"config": {

"issuer": "https://login.microsoftonline.com/TENANT-GUID/v2.0",

"clientId": "globex-client-id",

"clientSecret": "globex-client-secret",

"useJwksUrl": "true",

"validateSignature": "true"

}

}'

Link the IdP to the organization

curl -X POST "https://auth.example-saas.com/admin/realms/saas-prod/organizations/ORG-ID/identity-providers" \

-H "Authorization: Bearer $ADMIN_TOKEN" \

-H "Content-Type: application/json" \

-d '"globex-entra"'

Pricing and the SSO Tax Debate

Which pricing tier SSO belongs in is both a technical question and a business controversy. The practice of locking SSO behind the top enterprise tier with a steep price jump is criticized under the term **SSO tax**. The [sso.tax](https://sso.tax) site publicly tracks the practice, turning it into an industry-wide debate.

- **Critics**: SSO is a security feature, and gating security behind premium pricing lowers the security level of the whole ecosystem. It is a social loss when smaller customers who want SSO give up because of price.

- **Defenders**: enterprise SSO genuinely incurs significant integration and support costs, and it is a natural price discrimination point for identifying customers with high willingness to pay.

The pragmatic 2026 compromise looks like this.

| Feature | Recommended placement |

| --- | --- |

| Social login, passkeys | All tiers |

| SAML/OIDC SSO (single IdP) | From mid tier, or as a low-cost add-on |

| SCIM provisioning | Enterprise tier |

| Audit log API, session policy customization | Enterprise tier |

Under pressure from the security community, the pattern "SSO itself in lower tiers, governance features (SCIM, audit, policy) in higher tiers" is gradually becoming the norm. The architectural implication is clear: **design the auth layer with per-tenant feature flags from day one, so SSO can be attached to or detached from any tier later.**

Tenant Isolation of Sessions and Permissions

Session isolation

One user can belong to two tenants (for example, a consultant who is a member of two customer workspaces). The tenant context of sessions and tokens must then be unambiguous.

- **Stamp tenant claims into tokens**: as with the organization claim shown above, every access token must carry "which tenant context does this token belong to."

- **Switch tenants by re-issuing**: when switching workspaces, do not reuse the existing token; issue a fresh token for the new tenant context.

- **Per-tenant session policy**: enterprise tenants demand their own policies such as "30 minutes idle, 8 hours absolute, lifetime tied to the IdP session." Externalize session policy into tenant settings.

Authorization isolation

Every API request must pass through a single middleware that enforces "token tenant equals resource tenant."

@Component

public class TenantIsolationFilter extends OncePerRequestFilter {

@Override

protected void doFilterInternal(HttpServletRequest req,

HttpServletResponse res,

FilterChain chain) throws IOException, ServletException {

String tokenTenant = jwtContext.organizationId(); // from token claims

String resourceTenant = tenantResolver.fromRequest(req); // from URL/resource

if (!tokenTenant.equals(resourceTenant)) {

auditLog.record("authz.cross_tenant_denied", tokenTenant, resourceTenant);

res.sendError(HttpServletResponse.SC_NOT_FOUND); // 404 instead of 403 to hide existence

return;

}

chain.doFilter(req, res);

}

}

Cross-tenant access attempts should be recorded as security events, and the convention is to respond with 404 to hide the very existence of the resource. When finer-grained permissions become necessary (document-level sharing and so on), consider extending to a ReBAC model ([OpenFGA](https://openfga.dev/docs), the [Google Zanzibar](https://research.google/pubs/pub48190/) family). Even then, the tenant boundary must remain the top-level dimension of the relationship graph.

Audit and Compliance

What enterprise customers' security teams demand:

- **Authentication event audit logs**: record login success/failure, SSO configuration changes, domain verification, and admin permission changes, and let tenant admins query/export their own tenant's slice (including an API for SIEM integration).

- **SOC 2 / ISO 27001 evidence**: SSO configuration change history and access control records are our audit material too. It must be distinguishable whether a change was made by a tenant admin or by our operators.

- **Data residency**: region requirements sometimes extend to authentication logs. Evaluate the possibility of regional separation of the auth layer at the architecture stage.

- **Session attestation**: you must be able to answer "when was departed employee D's last access?" Store session creation/expiry/revocation events together with a retention policy.

A Collection of Real-World Scenarios

Let us validate the design against scenarios you will actually meet in operation.

**Scenario 1 — A customer migrates from Okta to Entra**

acme replaces its IdP. If your user matching key was the IdP subject, everyone gets duplicated as new users. Lesson: make "issuer+subject" the primary matching key for federated users, keep verified-domain email as a secondary link key, and ship an IdP migration procedure (admin bulk re-mapping of old IdP links) as a product feature.

**Scenario 2 — Domain consolidation after an acquisition**

globex acquires initech, and initech.com users join the globex organization. You need domain re-verification, member merging with the existing initech organization, and handling of in-flight sessions (forced re-authentication). Without cross-organization member-move APIs and audit logs, this becomes manual-labor hell.

**Scenario 3 — IdP outage means nobody can log in**

If acme's Okta goes down, every acme user is locked out. This is intended behavior (authentication ownership lies with the customer), but you must have break-glass admin accounts, status page guidance, and federation health monitoring that can demonstrate "the IdP outage is not our outage."

**Scenario 4 — API tokens after SSO enforcement**

acme turns on SSO enforcement, yet personal API tokens previously issued by acme employees still work. You must decide whether the SSO enforcement policy includes an option for "invalidate existing tokens + re-issue based on IdP sessions." Most enterprise security teams expect invalidation.

Closing Thoughts

Multi-tenant SSO is not "wiring up a SAML library" — it is a product architecture problem entangling tenant modeling, trust verification, lifecycle, and billing. To summarize the essentials:

1. SSO in B2B SaaS means per-tenant external IdP federation. Put a broker layer between the app and the IdPs, and let the app see only a single issuer.

2. For the tenant model, single realm + Keycloak Organizations (26+) is the default; split out only the exceptions that need hard isolation.

3. Email-domain HRD and DNS domain verification are the foundation of routing trust. Unverified domain mapping is an account-takeover channel.

4. JIT is the starting point; SCIM is the enterprise end state. Be honest with customers about the deprovisioning gap.

5. The self-service onboarding wizard (metadata auto-parsing, test login, break-glass) determines your SSO support cost.

6. Mind the SSO tax debate: design auth features as per-tenant flags so you can adapt to pricing policy changes.

In the next post, we tackle the final hard problem of this architecture: Single Logout — designing logout, which is harder than login.

References

- [Keycloak Server Administration — Managing Organizations](https://www.keycloak.org/docs/latest/server_admin/index.html#_managing_organizations)

- [Keycloak Documentation](https://www.keycloak.org/documentation)

- [Keycloak Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)

- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)

- [SAML 2.0 Core Specification (OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)

- [RFC 6749 — The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)

- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)

- [RFC 7644 — SCIM: Protocol](https://datatracker.ietf.org/doc/html/rfc7644)

- [OAuth 2.1 Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/)

- [The SSO Wall of Shame (sso.tax)](https://sso.tax)

- [OpenFGA Documentation](https://openfga.dev/docs)

- [Google Zanzibar paper](https://research.google/pubs/pub48190/)

- [FIDO Alliance — Passkeys](https://fidoalliance.org/passkeys/)

현재 단락 (1/262)

If you run a B2B SaaS, there is one question you will inevitably hear during enterprise sales: "Do y...

작성 글자: 0원문 글자: 19,728작성 단락: 0/262