8000 Keep the organization invitation through broker login by SferaDev · Pull Request #52183 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Keep the organization invitation through broker login - #52183

Merged
pedroigor merged 1 commit into
keycloak:mainfrom
SferaDev:invite-org-broker-membership
Sep 14, 2026
Merged

pedroigor merged 1 commit into
keycloak:mainfrom
SferaDev:invite-org-broker-membership

Conversation

@SferaDev
Copy link
Copy Markdown
Contributor

Closes #52181

The organization from an invitation was only ever on the request context, so it was gone by the time the invitee came back from the broker. This pins it to the authentication session when the session is created, carries it across resetFlow alongside the notes already preserved there, and lets IdpAddOrganizationMemberAuthenticator accept a pending invitation for the user's email as authorization on its own.

Matching on the invitation email is what keeps this safe: opening an invitation and then authenticating as somebody else finds no invitation and falls through to the existing broker check.

Membership is added as unmanaged, matching InviteOrgActionTokenHandler rather than the registration form. Happy to switch it if you would rather the two invitation paths agree the other way.

This does not cover an invitee who is already federated with the broker, since first broker login does not run for them. That looks like it belongs with #48912.

The new test uses "Back to Login" to reach the broker because register.ftl renders no broker buttons. It passes with the change and fails without it.

I used an AI agent while working on this change.

Copilot AI balanced review requested due to automatic review settings August 28, 2026 14:20
@SferaDev
SferaDev requested review from a team as code owners August 28, 2026 14:20
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Preserves organization invitations across first-broker login so invited users can join through realm-level identity providers.

Changes:

  • Stores and preserves the organization ID across authentication resets.
  • Accepts matching pending invitations during broker onboarding.
  • Adds an integration test for the successful flow.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
OrganizationInvitationLinkTest.java Tests broker-based invitation acceptance.
IdentityBrokerService.java Preserves organization state across flow reset.
AuthorizationEndpoint.java Copies organization context into the authentication session.
IdpAddOrganizationMemberAuthenticator.java Consumes matching invitations and adds unmanaged membership.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +63 to +64
provider.addMember(organization, user);
provider.getInvitationManager().remove(invitation.getId());
Copilot AI review requested due to automatic review settings August 28, 2026 14:25
@SferaDev
SferaDev force-pushed the invite-org-broker-membership branch from c6c9e7e to bcd4950 Compare August 28, 2026 14:25
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java:412

  • Only preserving the organization drops the invitation's redirect URI. Registration links do not put a redirect_uri in the authorization request, and the normal registration path later applies InviteOrgActionToken#getRedirectUri; this broker path bypasses that code, so invitations using an organization's custom redirect URL return to the account client's default URL instead. Preserve and apply the invitation redirect state across the broker round trip as well.
        OrganizationModel organization = session.getContext().getOrganization();

        if (organization != null) {
            authenticationSession.setAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId());

services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpAddOrganizationMemberAuthenticator.java:70

  • This consumes an organization invitation without emitting the INVITE_ORG success event. Both existing invitation-consumption paths emit that event (including user and organization details), so broker-accepted invitations will be absent from event listeners and audit logs; emit the same event before completing the flow and assert it in the new test.
            provider.addMember(organization, user);
            provider.getInvitationManager().remove(invitation.getId());

@SferaDev
SferaDev force-pushed the invite-org-broker-membership branch from bcd4950 to 377b7dd Compare August 28, 2026 16:21
Copilot AI review requested due to automatic review settings August 28, 2026 16:21
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings August 28, 2026 19:51
@SferaDev
SferaDev force-pushed the invite-org-broker-membership branch from 377b7dd to 70101dc Compare August 28, 2026 19:51
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@SferaDev
SferaDev force-pushed the invite-org-broker-membership branch from 70101dc to b77b399 Compare August 28, 2026 19:59
Copilot AI review requested due to automatic review settings August 28, 2026 19:59
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#employee2Test

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#testUserAttributeStatementMapperUserGroupsAggregate

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#testUserAttributeStatementMapperGroupsNoAggregate

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#singleLoginAndLogoutSAMLTest

Keycloak CI - Adapter IT Strict Cookies

org.openqa.selenium.TimeoutException: 
Expected condition failed: waiting for wrapped: text ('principal=bburke') to be present in text in element found by By.xpath: //body
(tried for 5 seconds with 500 milliseconds interval)
Build info: version: '4.46.0', revision: 'df5a634 df5a6341cbb5f8f06b836bd595752775627c80dc'
System info: os.name: 'Linux', os.arch: 'amd64', os.version: '6.17.0-1022-azure', java.version: '21.0.12.1'
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostTestCom F438 positeRoleForUser

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostEmptyConsumerPostURL

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#testUserAttributeStatementMapperGroupsAggregate

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#testReloginWithInvalidAuthSessionCookie

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostTest

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostSigTransientTest

Keycloak CI - Adapter IT Strict Cookies

java.lang.NullPointerException: Cannot invoke "String.contains(java.lang.CharSequence)" because the return value of "org.openqa.selenium.WebDriver.getPageSource()" is null
	at org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest.assertForbiddenLogin(SAMLServletAdapterTest.java:547)
	at org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest.salesPostSigTransientTest(SAMLServletAdapterTest.java:1137)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.jboss.arquillian.junit.Arquillian$8$1.invokeMethod(Arquillian.java:319)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#testUserAttributeStatementMapperUserGroupsNoAggregate

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostSigTest

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#disabledClientTest

Keycloak CI - Adapter IT Strict Cookies

org.openqa.selenium.TimeoutException: 
Expected condition failed: waiting for wrapped: text ('Login requester not enabled') to be present in text in element found by By.xpath: //body
(tried for 5 seconds with 500 milliseconds interval)
Build info: version: '4.46.0', revision: 'df5a634 df5a6341cbb5f8f06b836bd595752775627c80dc'
System info: os.name: 'Linux', os.arch: 'amd64', os.version: '6.17.0-1022-azure', java.version: '21.0.12.1'
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#testMultipleTabsParallelLogin

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: Expected page 'login-login' to be loaded, but currently on page 'null' after timeout
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
...

Report flaky test

Copilot AI review requested due to automatic review settings August 29, 2026 06:15
@SferaDev
SferaDev force-pushed the invite-org-broker-membership branch from b77b399 to 1a3028c Compare August 29, 2026 06:15
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

@@ -52,6 +58,26 @@ protected void authenticateImpl(AuthenticationFlowContext context, SerializedBro
return;
}

InviteOrgActionToken invitation = getPendingInvitation(session, organization, user);
@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.broker.KcOidcBrokerTest#loginWithExistingUserWithBruteForceEnabled

Keycloak CI - Java Distribution IT (windows-latest - temurin - 17)

org.opentest4j.AssertionFailedError: Expected page 'login-error' to be loaded, but currently on page 'null' after timeout
	at org.keycloak.testsuite.pages.AbstractPage.waitForPage(AbstractPage.java:62)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:47)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at org.jboss.arquillian.graphene.proxy.GrapheneProxyHandler.invokeReal(GrapheneProxyHandler.java:143)
...

Report flaky test

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

Closes keycloak#52181

Signed-off-by: Alexis Rico <sferadev@gmail.com>
@SferaDev
SferaDev force-pushed the invite-org-broker-membership branch from 1a3028c to d2a5f84 Compare August 31, 2026 06:00
Copilot AI review requested due to automatic review settings August 31, 2026 06:00
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@pedroigor pedroigor left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @SferaDev.

@pedroigor
pedroigor merged commit cbdd0bd into keycloak:main Sep 14, 2026
94 checks passed
@pedroigor
Copy link
Copy Markdown
Contributor

@SferaDev I would appreciate an update to the docs with something like this, perhaps also make clear the binding between the invite token and the auth session, and the final check on match the email from the (authenticated) user and from the token. Wondering if we should make very clear that the assumption is that as long as the user can authenticate using the same email, they will be able to join the org.

When a new user follows an invitation link and then authenticates using an identity provider during registration, the invitation remains bound to that authentication session across the broker redirect and first broker login flow. After successful broker authentication, the user joins the organization targeted by the invitation, and the invitation is consumed.

In this invitation-driven broker onboarding path, membership is created as Unmanaged, consistent with existing-user invitation acceptance. The organization is determined by the invitation link itself, and invitation consumption is authorized only when the resulting user email matches the invitation email. This behavior applies whether the selected identity provider is linked to the organization or is available as a realm-level provider during registration.

@SferaDev
Copy link
Copy Markdown
Contributor Author

@pedroigor What about something like this? #52755

xata-bot pushed a commit to xataio/xata that referenced this pull request Oct 6, 2026
* [Roles] Treat removing a user from a group they are not in as done

Keycloak 26.8 answers a DELETE on an organization group member who is not in
the group with 400 "User not a member" (keycloak/keycloak#52588), where 26.7
answered 204. Changing a role clears every other role group, and removing a
member clears all of them, so almost every call lands on a group the user was
never in. On 26.8 each of those would fail the role change or member removal.

Match the message as well as the status, since 400 also covers malformed
requests. Verified against a Keycloak built from main: 26.7.3 returns 204 and
the 26.8 build returns 400 {"errorMessage":"User not a member"}.

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* [Auth] Keep Keycloak 26.8 domain routing through organization updates

Keycloak 26.8 moves an organization's SSO routing off the identity provider
config and onto each domain, as identityProviderAlias and autoRedirect
(keycloak/keycloak#52401). Its migration copies the existing kc.org.domain and
redirect-on-email-match config across and deletes it. From then on an
organization PUT replaces every domain with what it carries, so a domain sent
without those fields loses its provider and redirect.

Domain carried only name and verified, so every organization update maki makes
(billing, pending SSO domains, SetOrganizationDomains) would silently turn SSO
enforcement off on 26.8.

Round-trip both fields, omitted when unset. 26.7.3 answers 400 "Unrecognized
field identityProviderAlias" for a domain carrying either, and never returns
them, so this changes nothing before the upgrade and must land before it.

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* Trim comments

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* [Keycloak] Upgrade to 26.8.0

Keycloak 26.8 moves organization domain routing off identity provider config
and onto each organization domain (keycloak/keycloak#52401). It deletes
OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE,
ORGANIZATION_EXCLUDED_DOMAIN_ATTRIBUTE and IdentityProviderRedirectMode, so
the extensions no longer compile, and its migration removes the config keys
DomainSso read, so the old logic would have found nothing to enforce.

DomainSso now reads the routing from the most specific matching organization
domain, as OrganizationAuthenticator does. A subdomain held without a provider
opts out of a wildcard that has one, which replaces the excluded-domains list.
assertsOwnDomain binds a provider to the domains routed to it across the
organizations it is linked to.

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* [Keycloak] Accept invitations through brokers with Keycloak's own step

Keycloak 26.8 keeps an organization invitation across the redirect to an
identity provider and has idp-add-organization-member accept it, whichever
provider the invitee used (keycloak/keycloak#52183). The xata capture and
accept authenticators existed only to do that, so the flows stop calling them.

The providers stay in the image for now. Keycloak cannot list the executions
of a flow that names a provider it does not have, and keycloak-config-cli
reads them before it rewrites the flow, so an image without the providers
fails the realm import that would have removed them. They go in a follow-up.

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* [SSO] Route providers through Keycloak 26.8 organization domains

Keycloak 26.8 keeps an organization's SSO routing on each domain, as
identityProviderAlias and autoRedirect, instead of in identity provider
config (keycloak/keycloak#52401). Its migration moves kc.org.domain and the
redirect-on-email-match flag across and deletes them, and every later write
of an organization-linked provider strips them again. The SSO service still
read and wrote those keys, so after the upgrade every provider would show no
domain and no enforcement, and turning enforcement on would silently do
nothing.

Read a provider's domain and enforcement from the domain routed to it, and
write routing through SetOrganizationDomains: CreateProvider routes the domain
once the provider is linked (Keycloak refuses a provider the organization is
not linked to), SetProviderEnforcement flips autoRedirect, and credential
rotation no longer carries routing because Keycloak leaves it in place.
Revoking a lapsed domain now unverifies and unroutes it in one write, which
removes the ordering the separate provider update needed.

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* [Keycloak] Trust SSO emails on verified domains and refuse unrouted providers

Keycloak 26.8 takes a brokered email's verified state from the provider's
email_verified claim and reads a missing one as false (keycloak/keycloak#50699),
even with trustEmail. Entra's userinfo never sends the claim, so its users
would be asked to verify an address their organization already controls.

xata-trust-domain-email, in post broker login, marks the email verified when an
organization's own provider asserted it on a verified domain routed to that
provider. The organization proved it controls the domain's DNS, and with it
the mail, so a verification mail would prove nothing more. It leaves the email
alone when it was changed on the review page, when it is not what this login
asserted, and for shared providers.

xata-require-domain-sso also stops treating an organization's provider that no
domain routes to as shared. That is what the domain recheck leaves once a
domain lapses, and it could otherwise assert any address.

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* Trim comments

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* [SSO] Rely on Keycloak 26.8 to keep a domain to one organization

Keycloak 26.8 adds a unique constraint on an organization domain's realm and
name. Two organizations verifying the same domain at once no longer both
succeed: the loser's update is rejected with 409, which a local run confirmed
across repeated concurrent claims. Before, uniqueness was checked in
application code only, so verification wrote the domain, listed every holder,
and backed itself out when it found another.

Map the 409 to ErrDomainAlreadyClaimed and let VerifyDomain return
ErrDomainClaimedElsewhere from it directly. That drops backOutIfHeldElsewhere,
the playbook section for recovering a domain held by two organizations, and
OrganizationsForDomain's list of holders, which is now OrganizationForDomain.

Also:
- CreateProvider and DeleteDomain read the provider off the domain's routing
  instead of listing every identity provider.
- SetOrganizationDomains matches Keycloak's one wording for each rejection.
- autoRedirect is always sent; identityProviderAlias stays omitted when unset,
  since 26.8 rejects an empty alias.

Claude-Session: https://claude.ai/code/session_01RvN9oeQKKP4e5H5TvnNa6h

* Disable SSO enforcement through domain routing in the admin gate test

Main added the test against redirect-on-email-match provider config, which
Keycloak 26.8 replaces with org domain autoRedirect.

* Keep SSO-brokered organization members managed on Keycloak 26.8

Keycloak 26.8 moves membership config onto each organization and identity
provider link. The link POST takes only the provider alias and creates the
link with autoMembership=true and membershipType=UNMANAGED, while links that
exist at upgrade time are migrated to MANAGED. Organizations that set up SSO
after the upgrade would therefore get unmanaged members, whose accounts
survive removal from the organization, unlike every SSO member today.

Set the link to autoMembership=true and MANAGED right after linking,
including when the link already exists, so every organization keeps the
behaviour it has on 26.7.

* Make every organization member unmanaged

Keycloak deletes a managed member's account when they are removed from the
organization or the organization is deleted. An account can belong to other
organizations and outlive any one of them, so no member should be managed.

Drop the PUT that set SSO links to MANAGED after linking. The link POST
already creates them with autoMembership=true and UNMANAGED, so the PUT has
nothing left to do for new links, and depending only on the POST keeps SSO
setup working against a Keycloak that predates the per-link endpoint. Links
that the 26.8 migration turns MANAGED are fixed once by a separate tool.

Keycloak adds an invitee who registers through the invitation link as a
managed member. Have the invitation role listener make them unmanaged after
the join commits, in its own transaction so a failure there is logged and
cannot stop the role grant.

* [SSO] Fix the merged SSO tests for domain routing

* [SSO] Describe enforcement on 26.8 domain routing in the lockout playbook

* [SSO] Leave a provider being created alone in the domain recheck

The recheck disabled any enabled provider no domain routed to, but
CreateProvider links a provider before routing its domain to it, so a
recheck landing between the two switched off a provider the create then
reported as working. Disable only providers whose domain is no longer
verified. Re-verify now routes before enabling, so a failure between the
two leaves the provider off.

* [SSO] Name the provider when enforcing one no domain routes to

Enforcing a provider whose domain the recheck revoked answered
"domain  is not verified" with no domain, since none routes to it.
Answer with ErrProviderUnbound as UpdateProvider does, and say in the
broker log that the provider is outside every domain routed to it.

* [SSO] Expect the unbound error when enforcing an unrouted provider

* [Keycloak] Vendor the 26.8.0 operator manifest and CRDs

Regenerated from keycloak-k8s-resources 26.8.0 rather than editing the
version strings: the operator moves to Quarkus 3.40.1 and sets a 10s
termination grace period, and the realm import CRD gains organization
links and per-domain routing. Our namespace templating, kube-linter
annotations, enableServiceLinks and tolerations are reapplied.

* [Keycloak] Turn off SCIM and block cluster node registration

Keycloak 26.8 turns the SCIM API on by default, and we don't use it, so
disable it at build time and in the Keycloak CR together. 26.8 also adds
an executor for the legacy register-node endpoint, which otherwise lets
a confidential client point management callbacks at any host; add it to
the SSRF profile with no allowed hostnames, since nothing registers
nodes.

* [Keycloak] Hold the realm import until Keycloak runs the current spec

Flux applies the Keycloak CR and the import-keycloak-realm Job together when
the image changes. The operator takes a while to replace the pods (update
compatibility Job, then a recreate or rolling update), and in that window the
old Keycloak still answers, so keycloak-config-cli's availability check
passes and it imports the new realm.json into the old version. On the 26.8
upgrade that fails on authenticators the old image lacks, or applies part of
the realm before the pod goes away.

Add a wait-for-keycloak init container that polls until the Keycloak CR's
Ready condition is True at its current generation and the StatefulSet has
rolled out that spec: observed its own generation, current revision equals
update revision, every instance ready, and the keycloak container image
equals the CR image. Ready and observedGeneration alone are not enough: the
operator computes status from the StatefulSet it read before reconciling, so
after deciding an update it reports Ready=True at the new generation while
the StatefulSet is only now being scaled down or rolled. The image and
rollout checks close that window. When only realm.json changes the CR and
StatefulSet are already settled and the wait passes on its first poll.

The init container reuses alpine/k8s, pinned by digest, under a dedicated
ServiceAccount allowed to get only the auth-keycloak Keycloak and
StatefulSet. realmImport.waitForKeycloak.timeoutSeconds bounds the wait.

* [Keycloak] Hold the realm import until Keycloak runs the current spec

Flux applies the Keycloak CR and the import-keycloak-realm Job together when
the image changes. The operator takes a while to replace the pods (update
compatibility Job, then a recreate or rolling update), and in that window the
old Keycloak still answers, so keycloak-config-cli's availability check
passes and it imports the new realm.json into the old version. On the 26.8
upgrade that fails on authenticators the old image lacks, or applies part of
the realm before the pod goes away.

Add a wait-for-keycloak init container that polls until the Keycloak CR's
Ready condition is True at its current generation and the StatefulSet has
rolled out that spec: observed its own generation, current revision equals
update revision, every instance ready, and the keycloak container image
equals the CR image. Ready and observedGeneration alone are not enough: the
operator computes status from the StatefulSet it read before reconciling, so
after deciding an update it reports Ready=True at the new generation while
the StatefulSet is only now being scaled down or rolled. The image and
rollout checks close that window. When only realm.json changes the CR and
StatefulSet are already settled and the wait passes on its first poll.

The init container reuses alpine/k8s, pinned by digest, under a dedicated
ServiceAccount allowed to get only the auth-keycloak Keycloak and
StatefulSet. realmImport.waitForKeycloak.timeoutSeconds bounds the wait.

* [Keycloak] Bind the realm import to the intended chart revision

* [Keycloak] Move invitation membership protection to the migration PR

* [SSO] Defer domain uniqueness cleanup until after the upgrade

* [Keycloak] Test realm import deployment ordering

* Test realm import readiness in CI and bound API requests

* Refresh domain ownership before disabling SSO providers

* Use existing chart tooling for realm import regression tests

* Replace the import polling guard with an explicit two-stage upgrade

* Keep upstream SCIM feature defaults without enabling it for the realm

* Separate brokered email trust from the Keycloak compatibility upgrade

* Defer the built-in invitation flow switch to the invitation rollout

* Document validation gates for the narrower upgrade boundary

* Remove the Keycloak upgrade playbook
xata-bot pushed a commit to xataio/xata that referenced this pull request Oct 8, 2026
* [Keycloak] Remove the organization invitation authenticators

No flow calls xata-capture-org-invitation or xata-accept-org-invitation any
more: Keycloak 26.8 accepts an invitation through a broker itself
(keycloak/keycloak#52183). Deploy only after the flows change is live, since
a realm import against an image without these providers fails while the
flows still name them.

OrgInvitationTokenHandler stays until keycloak/keycloak#52397 is released.

* [Keycloak] Drop the invitation token handler now that 26.8 sets the organization

Keycloak 26.8 includes keycloak/keycloak#52397, which puts the inviting
organization on the request context in handleToken as well, so the
confirm-membership page gets org without our override.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Organization invitation is lost when the invitee signs up with a realm-level identity provider

4 participants

0