One Keycloak to Rule Them All - It Was Just a Login Page. What Could Possibly Go Wrong?

How we ended up using Keycloak for authentication and Single Sign-On across multiple products. One product -> more products -> duplicate login headaches -> Keycloak -> SSO finally works.

It all started with a simple requirement: users should be able to log in.

Easy enough. 😎

We had one product, one login page, and one backend that knew who everyone was. Life was good.

Then we built a second product.

“Can users log in with the same account?”

Sure. That’s what SSO is for. No big deal.

Then came a third product.

“Can users switch between all three without logging in again?”

Of course!

At this point, we still thought authentication was going to be the easy part.

Cute. 🙃

via GIPHY

The obvious solution

Our first instinct was to create a shared authentication service.

All products would call the same login API, receive a JWT, and use it to authenticate requests.

Something like this:

    Shared Auth Service
             |
   ┌─────────┼─────────┐
   |         |         |
Support   Documents  Workflows

Simple. Beautiful. Probably doable in a weekend.

Except that authentication is never just authentication.

via GIPHY

Someone wants password reset.

Someone wants MFA.

Someone wants Google login.

Then an enterprise customer asks if employees can log in using Microsoft Entra ID.

And suddenly our little authentication service needs password policies, identity federation, session management, token rotation, account linking, and a security audit.

Congratulations. We’ve accidentally started building an identity provider. 💀

We could keep going, of course.

Or we could use something that already solves these problems.

via GIPHY

Enter Keycloak

Of course, we weren’t the first developers to encounter this problem.

There are already plenty of identity-as-a-service solutions out there, like Auth0, Amazon Cognito, and Microsoft Entra External ID.

And honestly, they’re pretty tempting.

via GIPHY

Register an account, configure a few applications, integrate their SDKs, and let someone else worry about keeping the authentication infrastructure alive.

Sounds like a great deal, doesn’t it? 😎

But we had two requirements that made the decision a little more interesting.

First, cost.

Managed identity platforms usually offer free tiers, but pricing and feature availability can change as the number of users, enterprise connections, or application requirements grow.

Auth0, for example, has a free plan, but some advanced capabilities belong to paid tiers.

We’re building an ecosystem with multiple products, potentially lots of users, and quite a bit of internal service communication.

The last thing we wanted was to discover that our authentication bill had become more interesting than our infrastructure bill 🫩.

Second, and more importantly, on-premises deployment.

Some enterprise customers want everything running inside their own infrastructure.

Not just the application.

Not just the database.

Everything. Including the identity provider.

A cloud-only authentication service becomes a problem when the customer’s environment isn’t supposed to depend on an external SaaS platform.

And telling an enterprise customer to “just expose your internal authentication to the internet” is probably not going to make the security team very happy. 🙃

That’s where Keycloak came in.

via GIPHY

Keycloak is an open-source identity and access management platform that supports OpenID Connect (OIDC), OAuth 2.0, SAML, identity brokering, and a bunch of other things we really didn’t want to implement ourselves.

More importantly for us:

  • It’s free and open source. No Keycloak software licensing fee or per-user charge for running the community edition.
  • It’s self-hostable. We can run it on our own infrastructure, whether that’s Kubernetes in the cloud or servers inside a customer’s data center.
  • We control the deployment. Configuration, data residency, upgrades, and infrastructure decisions stay in our hands.

Of course, free software doesn’t mean free operations.

We still have to maintain Keycloak, manage its database, configure high availability, monitor performance, apply security patches, and deal with upgrades.

Basically, we traded a SaaS subscription for operational responsibility.

Classic engineering. We always find a way to give ourselves more work. 😂

via GIPHY

But for our requirements, that trade-off made sense.

We wanted the same identity architecture to work across both cloud and on-premises deployments, without tying the entire product ecosystem to a managed identity provider.

So we decided to use Keycloak.

The important part for us was simple: all our products could delegate user authentication to one place.

          Keycloak
     Identity Provider
              |
    Login / SSO Session
              |
   ┌──────────┼──────────┐
   |          |          |
Support    Documents   Workflows

Now each product can focus on its own business logic.

Support handles customer conversations.

Documents handles document processing.

Workflows handles business processes.

And Keycloak handles the question: “Who is this person?”

At least that’s the plan.

via GIPHY

One realm, multiple clients

Keycloak comes with a few concepts we need to understand.

First, a realm.

Think of it as an isolated identity space containing users, clients, authentication settings, and roles.

We decided to use one realm for our entire product ecosystem.

Realm: my-platform

Why not one realm per product?

Because we want shared user identities and Single Sign-On. Putting every product in a different realm would make that unnecessarily complicated.

Next, clients.

Each application that integrates with Keycloak needs a client configuration.

For example:

my-platform
|
├── support-web
├── documents-web
└── workflows-web

Each frontend is registered as an OpenID Connect client.

For a browser-based SPA, we’d typically configure:

  • Client authentication: Off (public client)
  • Standard flow: Enabled
  • PKCE method: S256
  • Valid redirect URIs: Restricted to the application’s actual callback URLs
  • Web origins: Restricted to trusted application origins

And no, putting a client secret in a React environment variable doesn’t make it secret.

The browser is not your vault. 😅

via GIPHY

For a server-rendered application or backend-for-frontend, we can instead use a confidential client and keep the credentials on the server.

So how does the login actually work?

Suppose Alice opens our Support application.

She isn’t authenticated, so Support redirects her browser to Keycloak.

The authorization request looks roughly like this:

GET /realms/my-platform/protocol/openid-connect/auth
  ?client_id=support-web                            # OAuth client ID registered in Keycloak.
  &response_type=code                               # Use Authorization Code Flow.
  &scope=openid profile email                       # Just your identity, profile, and email. Nothing suspicious. 👀
  &redirect_uri=https://support.example.com/callback # Send the user back home after the identity interrogation.
  &state=<random-state>                             # CSRF protection. Because apparently we can't trust anyone.
  &code_challenge=<pkce-challenge>                   # Proof that whoever started this mess should finish it.
  &code_challenge_method=S256                       # SHA-256. Because "trust me, bro" isn't a security standard.

The browser lands on Keycloak’s login page.

Alice enters her credentials, completes any required authentication steps, and Keycloak redirects her back with an authorization code.

https://support.example.com/callback
  ?code=abc123
  &state=xyz789

Support verifies the returned state, then exchanges the code for tokens using the original PKCE verifier.

POST /realms/my-platform/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=support-web                               # It's me again. Remember me?
&code=<authorization-code>                           # Here's the code you just gave me. Please don't pretend you forgot.
&redirect_uri=https://support.example.com/callback  # Same callback as before. additional protection 🫤
&code_verifier=<original-verifier>                   # Proof I started this login. "Trust me, bro" wasn't convincing enough.

And there we have it.

An access token, an ID token, and, depending on configuration, a refresh token.

No password handling in our application.

No custom login database.

No homemade authentication protocol held together by optimism and Stack Overflow snippets.

Beautiful. 🎉

via GIPHY

One small detail: the login library should handle the state and PKCE mechanics. Please don’t copy the example above and start inventing your own cryptography over lunch 🙃.

The magic of Single Sign-On

Now Alice opens Documents in another tab.

Documents redirects her to Keycloak, just like Support did.

But something interesting happens.

Keycloak already has an active SSO session for Alice in the browser.

So instead of asking for her password again, Keycloak can issue a new authorization code for Documents.

Alice
 |
 | Opens Support
 v
Keycloak → Login → Support
 |
 | Opens Documents
 v
Keycloak → Existing SSO Session → Documents

Notice that Documents still performs its own OIDC authorization flow.

It doesn’t copy Support’s token.

It gets tokens issued for its own client.

That distinction matters a lot, although we’ll save the really interesting consequences for Part 2.

Also, SSO isn’t a magical promise that users will never see another login screen. Session expiration, authentication policies, browser restrictions, and explicit reauthentication requirements can all change the experience.

Still, for our everyday use case, it works nicely.

One login, multiple products.

Finally!

via GIPHY

Wait, authentication isn’t authorization?

Here’s where things get interesting again.

Alice can successfully authenticate with Keycloak.

But does that mean she can access every product?

Not necessarily.

Let’s say Alice belongs to Acme Corp.

Acme subscribes to Support and Documents, but not Workflows.

Alice should be able to log into Support and Documents. But Workflows should reject access unless she has the necessary entitlement.

Keycloak verifies Alice’s identity.

Our products still need to decide what Alice is allowed to do.

Authentication answers: Who are you?

Authorization answers: What are you allowed to do here?

These sound suspiciously similar until you accidentally confuse them in production. 💀

We can store shared identity information, roles, and organization memberships in Keycloak.

But product-specific permissions, subscription entitlements, and resource access can remain within our own services.

For example, logging in doesn’t automatically give Alice permission to delete another employee’s documents.

And thank goodness for that.

via GIPHY

What about multiple organizations (multi tenants)?

Naturally, our platform also needs to support multiple organizations.

Alice might belong to both Acme and Globex.

So a single user identity isn’t enough. Our APIs also need to know which organization Alice is acting under.

Recent Keycloak versions provide Organizations support, including organization membership and organization-related token claims.

For example, with the appropriate organization scope and mapper configuration, a token might contain membership information like:

{
  "sub": "user-123",
  "organization": {
    "acme": {},
    "globex": {}
  }
}

That’s an illustrative payload, not a promise about the exact claims in every Keycloak configuration.

The application still needs to determine the active organization and enforce tenant-specific permissions.

Knowing Alice belongs to both organizations does not mean an API request should be able to freely mix their data.

via GIPHY

That would be… unfortunate. 😬

And what about logout?

Login was the fun part.

Logout is where distributed applications remind you that they are, in fact, distributed.

We have a Keycloak SSO session, individual application sessions or tokens, and possibly several products open at once.

Now imagine Alice clicks Logout in Support.

Keycloak ends her SSO session.

Great!

But Documents and Workflows might still have their own application sessions.

So Alice is logged out of Support, but another product might continue treating her as authenticated.

via GIPHY

That’s not exactly the Single Logout experience we were hoping for. 🤨

Fortunately, Keycloak supports something called OIDC Back-Channel Logout.

And this is actually pretty useful.

Keycloak can tell everyone it’s time to leave

The idea is simple.

When a user’s Keycloak session is terminated, Keycloak can send a logout notification directly to the participating applications.

Not through the browser.

Not through some questionable JavaScript event shared between unrelated domains.

Through a backend-to-backend HTTP request.

For example:

               Alice
                 |
            Clicks Logout
                 |
                 v
              Keycloak
           Ends SSO Session
                 |
     ┌───────────┼────────────┐
     |           |            |
     v           v            v
 Support      Documents    Workflows
 Backend      Backend      Backend
     |           |            |
     v           v            v
 Invalidate   Invalidate   Invalidate
  Session      Session      Session

Keycloak sends an HTTP POST containing a signed Logout Token to each participating client’s registered Back-Channel Logout URL.

Each application validates that token, identifies the affected session, and invalidates its local session.

So when Alice logs out, the other products can be notified without requiring their browser tabs to be open or active.

Much better 😘

How do we configure it?

In the Keycloak client settings, we can configure a Backchannel logout URL.

For example:

Client: documents-web

Front channel logout: Off

Backchannel logout URL:
  https://documents.example.com/auth/backchannel-logout

Backchannel logout session required: On

With the session ID claim enabled, Keycloak can include the sid claim in the Logout Token, allowing our backend to identify the specific application session.

The request looks roughly like this:

POST /auth/backchannel-logout
Content-Type: application/x-www-form-urlencoded

logout_token=<signed-logout-jwt>

Our backend needs to validate the Logout Token properly, including its signature, issuer, audience, and logout-specific claims, before invalidating the matching session.

Don’t just decode the JWT, read the sid, and delete whatever session it points to.

That would be an interesting way to turn a logout endpoint into a denial-of-service feature. 💀

via GIPHY

One more thing: this works naturally for applications with backend-managed sessions.

For a pure frontend SPA, Keycloak cannot directly call JavaScript running inside a browser tab.

We’d need a backend component to receive the notification and propagate the logout state to the frontend, perhaps through SSE, WebSocket, or session checks.

But what about existing access tokens?

Here’s the catch.

Back-Channel Logout notifies applications that a session has ended.

It does not automatically make every previously issued JWT fail local signature validation.

Suppose Alice has an access token valid for another five minutes.

If an API only checks the token’s signature, issuer, audience, and expiration, that token might still be accepted until it expires.

So we still need a strategy for token invalidation.

Depending on our security requirements, that might include short-lived access tokens, session-aware validation, a token or session denylist, and appropriate refresh-token handling.

Back-Channel Logout gives us a useful way to coordinate application logout.

But we still have to decide how quickly that logout should affect API access.

Because apparently even logging out has eventual-consistency problems. 🙃

So, are we done?

At this point, we’ve accomplished quite a bit.

We have one identity provider, multiple independent products, shared user accounts, and a proper SSO experience.

We no longer need to build and maintain our own authentication infrastructure.

Users can move between products without constantly entering their passwords.

And with Back-Channel Logout, participating applications can coordinate session termination instead of pretending they all live in the same browser tab.

For once, the architecture diagram and the actual user experience agree.

Then someone from the Support team asks:

“Hey, can we fetch processed documents directly from the Documents API?” 🤨

Sure, that sounds useful.

“Can we just forward Alice’s access token?”

Hmm.

That’s an interesting question.

And unfortunately, the answer is not as simple as we’d hoped.

via GIPHY

Next: Part 2 — Our Products Started Talking to Each Other. Security Was Not Amused.


Copyright © 2022. All rights reserved.