Repository navigation
Replies: 8 comments 6 replies
|
@wschomburg I'd welcome your feedback on this one, thank you |
|
This seems like a very comprehensive proposal that mostly covers our usecase! Thanks for the work! There is one small note here; for shared IdPs, we would prefer org membership to be determined by a specific claim value, rather than the email address. I'm not sure that's easy to integrate here, but it could be a situation that happens more often. |
|
Thanks @vramik , it covers well the requirements from #47554 |
|
Thanks @vramik, for your work.
|
|
@vramik One additional question about the process to choose the right org. We are using the IDP login_hint a lot to provide links that directly forward users to the customer IDP, without the extra step of choosing the IDP. This is used for internal customer app portals (on their infrastructure) and for dedicated customer frontends from one of our apps. Will it work with the flows you described, or do you plan to also provide an org_hint query parameter? |
|
The original post above has been updated to incorporate community feedback. Here's what changed and who drove it: Contributors: Key changes:
Previous version did not have domain-gated AM and used a flat |
|
Sorry for jumping in late, guys. Overall, the approach sounds solid, but I do not think we should allow different organizations to share the same domain. If you think otherwise, can you please share the use case? |
|
I'd like to gather feedback on #53115. Would you find this capability useful? Should it be configurable? Please share your thoughts here or in the issue. You can upvote the issue by adding a 👍 there — it will help us decide on priorities. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Context
This is a behavioral specification for Keycloak Organizations when three enhancements are implemented together:
DOMAIN.IDP_IDforeign key)ORG_IDENTITY_PROVIDERjoin table withauto_membershipandmembership_type)Two core design principles:
Legend
Data Model
Constraints:
DOMAIN.IDP_IDmust reference a validIDENTITY_PROVIDER(FK constraint, ON DELETE SET NULL)UNIQUE(REALM_ID, NAME)— each domain belongs to exactly one orgTwo Decoupled Concerns
DOMAIN.IDP_ID(domain → IdP)organization:aliasscope, user selectionIdP Routing (domain → IdP)
Lookup for a given email domain:
ORG_DOMAINrow matching that domain (or wildcard — most specific wins)IDP_IDis set ANDAUTO_REDIRECTis true → automatically redirect to that IdPIDP_IDis set ANDAUTO_REDIRECTis false → show login page with the IdP as an option (no automatic redirect)IDP_IDis NULL → no federated IdP → show password formNo need to check multiple rows per domain —
ORG_DOMAIN.NAMEis unique per realm.AUTO_REDIRECTreplaces the old per-IdPkc.org.broker.redirect.mode.email-matchesconfig. Moving it to the domain allows different redirect behavior per domain — e.g.,acme.comauto-redirects to EntraID whilepartner.comshows a login page with multiple IdP options.Org Context Resolution
After IdP callback, the system determines org context.
Resolution priority:
organization:alias→ use that orgORGANIZATION_ATTRIBUTE→ set by previous step (e.g., org login page)organization(singular) → show org selection UIorganization:*→ include all orgs in tokenResolution Flows
A. Browser Login — SP-Initiated (OrganizationAuthenticator)
DOMAINrow matching domainIDP_IDset +AUTO_REDIRECT= true → redirect to IdPIDP_IDset +AUTO_REDIRECT= false → show login page with IdP option (no redirect)IDP_ID→ show password formgetMatchingDomain()logic)organization:aliasscope or org login page, store in auth session noteB. After IdP Callback (configureOrganization)
ORGANIZATION_ATTRIBUTE→ use that orgC. Post-Broker Membership (IdpAddOrganizationMemberAuthenticator)
If
configureOrganizationresolved a specific org, only process that org (if it is linked to this IdP). Otherwise (no org resolved, e.g. IdP-initiated), iterate all orgs linked to this IdP viaORG_IDENTITY_PROVIDER:For each org:
ORG_DOMAIN) → add member with configured MT (domain-less org = domain-agnostic)ORG_DOMAINfor this org):D. Org Context for Token (post-membership)
organization:alias→ that orgorganization→ prompt or use default orgorganization:*→ all orgs in tokenScenario Matrix
Group 1: SP-Initiated IdP Login — New User
Scenario 1.1 — Simple: One IdP, One Org, One Domain
Setup: Org-A owns
acme.com(DOMAIN.ORG_ID = Org-A,IDP_ID = IdP-X). Join:(Org-A, IdP-X, AM=true, MT=MANAGED).Action: New user enters
alice@acme.comon login page.Flow: DOMAIN lookup →
acme.com→ IdP-X → redirect. After callback, IdP-X linked to one org → set Org-A as context. Post-broker: AM=true,acme.comowned by Org-A ✓ →addManagedMember(Org-A, alice).Final state: Alice is MANAGED member of Org-A. Equivalent to today's behavior.
Scenario 1.5 — Different Domains, Different IdPs, Same Org
Setup: Org-A owns
acme.com(IDP_ID = IdP-X) andpartner.com(IDP_ID = IdP-Y). Join:(Org-A, IdP-X, AM=true, MT=MANAGED),(Org-A, IdP-Y, AM=true, MT=UNMANAGED).Action A:
alice@acme.com→ IdP-X → MANAGED in Org-A.Action B:
bob@partner.com→ IdP-Y → UNMANAGED in Org-A.Final state: Different domains → different IdPs → different membership types within same org. This is the #47554 use case.
Scenario 1.6 — Non-Federated Domain
Setup: Org-A owns
acme.com(IDP_ID = IdP-X) andinternal.acme.com(IDP_ID = NULL).Action:
bob@internal.acme.comon login page.Flow: DOMAIN →
internal.acme.com→ IDP_ID = NULL → no redirect → password form → no broker flow → no auto-membership.Final state: Bob is realm user, not member of Org-A. Must be invited or manually added.
Scenario 1.7 — Wildcard Domain with Specific Override
Setup: Org-A owns
*.acme.com(IDP_ID = IdP-X) andhr.acme.com(IDP_ID = IdP-Y).Action:
carol@hr.acme.comlogs in.Flow: Both patterns match → most specific wins →
hr.acme.com→ IdP-Y. Post-broker via IdP-Y → Org-A AM=true + domain ✓ → add as configured.Final state: Carol in Org-A, authenticated via IdP-Y (HR-specific IdP). Wildcard precedence from
Organizations.getMatchingDomain()carries over.Scenario 1.8 — Multiple Domains, Different IdPs, One Non-Federated
Setup: Org-A owns
acme.com(IDP_ID = IdP-X),partner.com(IDP_ID = IdP-Y),internal.acme.com(IDP_ID = NULL). Join:(Org-A, IdP-X, AM=true, MT=MANAGED),(Org-A, IdP-Y, AM=true, MT=UNMANAGED).Action A:
alice@acme.com→ IdP-X → MANAGED.Action B:
bob@partner.com→ IdP-Y → UNMANAGED.Action C:
carol@internal.acme.com→ NULL → password form → no auto-membership.Note: No "ANY" catch-all needed. Each domain declares its IdP or null. This replaces the old
kc.org.domain=ANYconfig attribute which was a workaround for the 1:1 domain-per-IdP limitation.Scenario 1.9 — Different Domains, Same IdP, Different Orgs (core multi-IdP scenario)
Setup: Org-A owns
acme.com, Org-B ownsglobex.com. BothDOMAIN.IDP_ID = IdP-X(same shared IdP). Join:(Org-A, IdP-X, AM=true, MT=MANAGED),(Org-B, IdP-X, AM=true, MT=UNMANAGED).Action:
alice@acme.comlogs in.Flow: DOMAIN →
acme.com→ IdP-X → redirect. After callback: IdP-X linked to two orgs. Post-broker: Org-A AM=true +acme.comowned by Org-A ✓ → MANAGED. Org-B AM=true +acme.comNOT owned by Org-B ✗ → skip.Final state: Alice MANAGED in Org-A only. Domain gate prevents cross-org membership —
acme.comis not Org-B's domain, so shared IdP alone doesn't create membership.Group 2: SP-Initiated IdP Login — Existing User
Scenario 2.2 — Existing User, Invited to Org-B, Shared IdP, Different Domains
Setup: Alice is UNMANAGED member of Org-B (invitation). Org-A owns
acme.com, Org-B ownsglobex.com. Shared IdP-X. Join:(Org-A, IdP-X, AM=true, MT=UNMANAGED),(Org-B, IdP-X, AM=false, MT=UNMANAGED).Action: Alice (
alice@acme.com) logs in via IdP-X.Flow: Post-broker: Org-A AM=true +
acme.comowned by Org-A ✓ → add. Org-B AM=false → skip (already member anyway).Final state: Alice UNMANAGED in both. Note: even if Org-B had AM=true,
acme.comis not owned by Org-B, so Alice would NOT be auto-added to Org-B via IdP. Invitation-based membership is unaffected.Scenario 2.3 — Existing User, MANAGED in Org-A, Shared IdP, Different Domain in Org-B
Setup: Alice is MANAGED in Org-A. Org-A owns
acme.com, Org-B ownsglobex.com. Org-B now linked to IdP-X:(Org-B, IdP-X, AM=true, MT=UNMANAGED).Action: Alice (
alice@acme.com) logs in via IdP-X.Flow: Post-broker: Org-A already member → skip. Org-B AM=true +
acme.comNOT owned by Org-B ✗ → skip.Final state: MANAGED in Org-A only. Domain gate prevents auto-addition to Org-B.
Scenario 2.4 — Membership Type NOT Upgraded on Login
Setup: Alice was invited to Org-A → UNMANAGED. Org-A has IdP-X with MT=MANAGED, AM=true. Org-A owns
acme.com.Action: Alice logs in via IdP-X for first time.
Flow: Post-broker: Org-A AM=true, MT=MANAGED, but Alice already a member → skip. UNMANAGED status preserved.
Final state: Alice stays UNMANAGED in Org-A.
Rationale: Membership type set at creation time only. Admin invited her intentionally as UNMANAGED. Silent upgrade to MANAGED would change her lifecycle semantics as a side effect of login — dangerous and surprising.
Group 3: IdP-Initiated Login (No Auth Session)
Scenario 3.1 — IdP-Initiated, Single Org
Setup: IdP-X linked only to Org-A. Org-A owns
acme.com.Action: IdP-X sends assertion for
alice@acme.com.Flow: No auth session → IdP-X has one org → set Org-A → add member per AM config. Same as SP-initiated.
Scenario 3.2 — IdP-Initiated, Shared IdP, Domain Disambiguates
Setup: IdP-X linked to Org-A (owns
acme.com) and Org-B (ownsglobex.com).Action: IdP-X sends assertion for
alice@acme.com.Flow: No auth session note.
configureOrganization(): IdP-X in multiple orgs, look upacme.com→ owned by Org-A → primary = Org-A. Post-broker: Org-A AM=true + domain ✓ → add. Org-B AM=true +acme.comNOT owned by Org-B ✗ → skip.Final state: Alice in Org-A only.
Scenario 3.4 — IdP-Initiated, Email Domain Not Owned by Any Org
Setup: IdP-X linked to Org-A (owns
acme.com) and Org-B (ownsglobex.com). User hasalice@external.com(not in anyDOMAINrow).Action: IdP-X sends assertion for
alice@external.com.Flow: No domain match. Post-broker: Org-A AM=true +
external.comNOT owned ✗ → skip. Org-B AM=true + NOT owned ✗ → skip.Final state: Alice exists in realm, no org membership. Domain gate prevents auto-addition when the email domain isn't owned by any org. If admin wants users with unknown domains auto-added, they should use a domain-less org (see 7.6) or route users explicitly via kc_idp_hint
Group 4: Invitation Flow
Related: #46914 — Allow invite-user to onboard new members as unmanaged. Membership type for invited users should be a configuration option at org level, not per-invite.
Scenario 4.1 — Invite to Org with Shared IdP (AM=false)
Setup: Org-B has IdP-X (AM=false). Org-A also has IdP-X (AM=true). Org-A owns
acme.com, Org-B ownsglobex.com.Action: Admin of Org-B invites
alice@acme.com.Flow: Invitation created, Alice accepts → addMember(Org-B, alice) as UNMANAGED. No broker flow.
Final state: Alice UNMANAGED in Org-B only. Later login via IdP-X → post-broker adds to Org-A (AM=true + domain ✓). Org-B (AM=false) skipped; already member.
Scenario 4.2 — Invite to Org, User Later Logs In via Shared IdP
Setup: Alice invited to Org-A (UNMANAGED) and Org-B (UNMANAGED) — both AM=false. IdP-X shared.
Action: Alice logs in via IdP-X.
Flow: Both AM=false → no new memberships. Already member of both → context per scope.
Final state: Unchanged. Both memberships preserved.
Scenario 4.3 — Invite MANAGED User from Another Org
Setup: Alice is MANAGED in Org-A. Admin of Org-B invites Alice.
Action: Alice accepts → addMember(Org-B, alice) as UNMANAGED.
Final state: MANAGED in Org-A, UNMANAGED in Org-B. Dual membership.
Group 5: Manual Admin Addition
Scenario 5.1 — Add Existing User to Org Manually
Unchanged.
POST /organizations/{orgId}/members→ UNMANAGED member. No IdP interaction, no domain gate.Group 6: Lifecycle Events
Scenario 6.1 — Org Deletion with MANAGED Members
Setup: Alice MANAGED in Org-A, UNMANAGED in Org-B. Bob MANAGED in Org-A only.
Action: Admin deletes Org-A.
Behavior: MANAGED →
removeUser(realm, member):MANAGED semantics are intentional. DOMAIN rows for Org-A are cascade-deleted. Tools to manage this include #48913 (API to change membership type), AM=false + invitation model, and the decision that membership type is never silently upgraded on login.
Scenario 6.2 — Admin Removes IdP from Org
Setup: IdP-X linked to Org-A (AM=true, MT=MANAGED). Alice and Bob are MANAGED via IdP-X.
Action: Admin unlinks IdP-X from Org-A.
Expected behavior:
ORG_IDENTITY_PROVIDER(Org-A, IdP-X)rowDOMAINrows whoseIDP_ID= IdP-X remain (domain still routes to IdP-X — routing is independent of org-IdP association)Rationale: IdP unlinking is config, not membership. Members can still log in via password or other IdPs.
Scenario 6.3 — User Email Change
Unchanged:
Scenario 6.4 — Domain Removed from Org
Action: Admin removes
acme.comfrom Org-A.Behavior:
DOMAINrow foracme.com(domain belongs to this org alone — no orphan question)@acme.comremain members (membership ≠ domain ownership)@acme.comusers → no new auto-additionsDomain removal doesn't revoke existing memberships — only affects future auto-routing and auto-membership.
Group 7: Edge Cases
Scenario 7.1 — Same User, Different Membership Types Across Orgs
Valid. Membership type is per org. Alice can be MANAGED in Org-A and UNMANAGED in Org-B.
Scenario 7.2 — Two IdPs for Same Domain
DOMAIN.IDP_IDis a single FK → one IdP per domain. If admin needs two IdPs for same domain, use a single federating IdP or split into subdomains.Scenario 7.3 — MT=MANAGED Conflict Across Orgs
(Org-A, IdP-X, MT=MANAGED)+(Org-B, IdP-X, MT=MANAGED)→ violates C1. Rejected at config time (409/400).Scenario 7.5 — User Belongs to Org Without Domain Match
Valid. Domain matching is for IdP routing and auto-membership only, not a membership requirement. Invited/manually-added users don't need matching domains.
Scenario 7.6 — Org Has No Domains
Setup: Org-A has no domains (no
DOMAINrows withORG_ID = Org-A). IdP-X linked with AM=true.7.6a — IdP-initiated (no resolved org): User logs in via IdP-X with kc_idp_hint. No org resolved by configureOrganization → resolved-org gate open → Org-A iterated → AM=true + no domains → domain gate skipped → add member.
7.6b — SP-initiated via another org's domain: User enters alice@acme.com → routed to IdP-X via Org-B's domain. configureOrganization resolves Org-B → resolved-org gate limits processing to Org-B → Org-A skipped (not the resolved org).
7.6c — Client requests organization:org-a: Resolved org = Org-A → processed → AM=true + no domains → domain gate skipped → add member.
Note: A domain-less org has no domain routing, so it cannot be reached via email-based IdP discovery. To direct users to it, use organization:alias scope, kc_idp_hint, or invitations. The resolved-org gate prevents surprise membership — a user logging in through Org-B's page will not silently join an
unrelated domain-less org.
Scenario 7.7 — Catch-All
Bare
*as a domain is not supported.validateDomain()rejects it (fails email validation foruser@*). Wildcard domains use the*.prefix (e.g.,*.acme.com) for subdomain matching within a known domain hierarchy.For "add everyone who comes through this IdP regardless of email domain", use a domain-less org (scenario 7.6): an org with no DOMAIN rows skips the domain gate entirely, achieving the same post-broker membership result.
Scenario 7.8 — Multiple Orgs Need Users from Same Email Domain
With 1:1 domain-to-org, a domain can only be owned by one org. For use cases where multiple orgs need users from the same email domain, two approaches:
alice@acme.comexplicitly (no domain ownership needed)Follow-ups (out of scope for this proposal)
org_hintquery parameter — @wschomburg's use case: pre-selecting org context for portal-based flows via login_hint. Separate enhancement.ORG_IDENTITY_PROVIDER(e.g.,MEMBERSHIP_CLAIM_NAME+MEMBERSHIP_CLAIM_VALUE). No design conflict with this proposal.All reactions