How we improved Organization Invites to Keycloak

Many discussions with the Keycloak maintainers and a lot of code reviews later, our biggest contribution to Keycloak to date has been merged

Author

Alexis Rico

Date published

Contributing a six-month feature to one of the largest open-source identity projects

At Xata, we use Keycloak for authentication. Like many SaaS companies, we rely heavily on its Organizations feature for multi-tenancy where each of our customers gets their own isolated workspace with dedicated identity controls. After hitting a frustrating limitation with organization invitations, we decided to fix it upstream rather than work around it.

Six months of back and forth, many discussions with the Keycloak maintainers at Red Hat and IBM and a lot of code reviews later, our biggest contribution to Keycloak to date has been merged 🎉

This post covers the problem we faced, why we chose to contribute upstream and the technical details of what we built.

What is Keycloak?

If you're not familiar with Keycloak, it's an open-source Identity and Access Management (IAM) solution that handles authentication, single sign-on, and user federation. Originally created at Red Hat, it joined the CNCF as an incubating project in 2023. It supports all the protocols you'd expect: OpenID Connect, OAuth 2.0, and SAML 2.0.

For SaaS platforms, Keycloak’s Organizations feature is especially useful. It provides built-in multi-tenancy, letting you manage all customers under a single, scalable setup instead of spinning up isolated environments for each one.

The problem: Invisible invitations

Organizations in Keycloak let you invite users via email. The flow works like this:

  1. An admin invites someone by email address
  2. Keycloak generates an action token (a signed JWT) and emails it to the invitee
  3. The invitee clicks the link, registers (if new) or logs in and joins the organization

The problem? Once you send an invitation, it disappears into the void. There's no way to:

  • See pending invitations
  • Check if an invitation expired
  • Resend or revoke an invitation
  • Know which invitations were accepted

This is particularly painful when inviting users who don't yet have a user account. Since no user entity exists, there's nothing to attach the invitation state to. The action token lives only in Keycloak's cache until it's used or expires with no visibility for administrators.

Why contribute upstream?

We could have worked around this. We could have built our own invitation tracking layer on top of Keycloak, intercepting API calls and maintaining our own database of pending invites. But that approach has costs:

Maintenance burden: Every Keycloak upgrade becomes a potential breaking change. You're now maintaining integration code indefinitely.

Sync problems: Your external state can drift from Keycloak's actual state. What happens when an invitation is accepted but your webhook fails?

Duplication: You're reimplementing logic that belongs in the identity layer. Every other Keycloak user with this problem does the same.

Contributing upstream means the feature lives where it belongs. It is tested by Keycloak's CI, maintained by the community and benefits everyone. The trade-offs are time and dedication to open source but the result is permanent infrastructure rather than a fragile workaround.

The technical challenge

The core challenge was architectural. Keycloak's action tokens are stateless JWTs, verified cryptographically and tied to user state. They are designed this way intentionally:

  • No database writes on token creation
  • Validation happens by checking the signature and user state at redemption time
  • Tokens are cached but not persisted

This works beautifully for existing users. The token encodes a user ID, and validation checks if the user's state has changed since issuance (for example, if the password was already reset). But for organization invitations to non-existent users, there is no user ID to encode. The invitation hangs in limbo.

What we built

The solution required changes across multiple layers of Keycloak:

REST API: Full CRUD for Invitations

We extended the existing OrganizationInvitationResource to support full invitation management:

GET /admin/realms/{realm}/organizations/{orgId}/invitations

List all invitations for an organization

GET /admin/realms/{realm}/organizations/{orgId}/invitations/{id}

Get a specific invitation

DELETE /admin/realms/{realm}/organizations/{orgId}/invitations/{id}

Revoke an active invitation

POST /admin/realms/{realm}/organizations/{orgId}/invitations/{id}/resend

Resend an invitation

The existing invite-user endpoint was modified to create a database record alongside generating the action token.

Admin Console UI

The Admin Console received a new "Invitations" tab within the Organizations section, displaying:

  • Email address of the invitee
  • Invitation status (pending, expired)
  • Sent date and expiration date
  • Actions (resend, revoke)

Token lifecycle integration

The trickiest part was integrating with the existing action token flow. When an invitation is accepted:

  1. The InviteOrgActionTokenHandler validates the token as usual
  2. We added a hook to mark the corresponding invitation record as accepted
  3. The user becomes an organization member

When an invitation is revoked:

  1. The database record is deleted
  2. If the action token is still in the cache, it's invalidated
  3. Attempting to use the token returns an appropriate error

The review process

Contributing to Keycloak is not a quick process and I expected that going in. The project has high standards, since this is identity infrastructure that protects countless applications. A few things stood out:

Architecture discussions: Pedro Igor, one of the core maintainers, raised the question of whether we should build a more generic PersistentActionEntity that could serve multiple use cases. We discussed the trade-offs and agreed to proceed with a focused implementation first.

Multiple iterations: The PR went through several changes as we addressed feedback. Review comments covered naming conventions, error handling, test coverage and API design.

Collaboration model: Pedro ended up co-authoring several commits, refining aspects of the implementation with improvements to the persistence layer that they could reuse in other features.

Patience required: From opening the PR in June to merging in November, the calendar time was substantial. Much of that was waiting for review cycles, which is normal for a project with this many contributors and priorities.

When will this ship?

The PR was merged into Keycloak's main branch on November 27, 2025. It will be part of version 26.5.0, expected in early 2026. If you want to try it now, it is available in Keycloak's nightly builds.

Reflections on contributing to large OSS projects

Six months is a long time to wait for a single feature. Was it worth it? Absolutely but with caveats:

Do it when the feature is core to your product. We use organization invitations to onboard new users to their teams. This wasn't a nice-to-have, it was blocking real workflows.

Budget for the long haul. Open-source projects have long review cycles. If you need something in two weeks, contributing upstream isn't the path.

Expect collaboration, not just review. The best outcome isn't "they merged my code unchanged", it's "the code improved through the process."

Try it out

If you are using Keycloak Organizations and have struggled with invitation visibility, this feature is for you. You can test it today using Keycloak's nightly channel or wait for the 26.5.0 release.

And if you are facing limitations in other open-source tools you depend on, consider contributing. It is slower than a workaround but the result is infrastructure that benefits everyone.

What's next

Looking ahead, we plan to keep contributing back to Keycloak and other open-source projects that form the backbone of our platform. These efforts help us improve the tools we depend on while we continue building what we aim to be the best database platform out there.

Related Posts