Priya Vijai Kalyan

Keycloak with Claude, Codex & A Whole Lot of Coffee

Hello. I've been working on a side project: a collection of tools I wished existed in a better form, and in some cases tools which don’t exist at all as far as I can tell. I initially started out building them to use only local storage (single page HTML app that could run in my browser and lived offline), but quickly realized that’s not a long term workable solution. As I built the backend, I adopted Auth0 for authentication; it was simple to get started with. I have some vague idea of making the hosted version public a few months down the line while open-sourcing it. As I used the tools I built, I realized that I was copying data back and forth with the AIs I use: Claude & Codex. The obvious next step was to expose these web apps I'd written to tools like Claude and Codex, so I could pull data out of them directly or have these tools co-work with me inside the apps. An MCP endpoint looked like the most useful path. So I started building one, and put Auth0 in front for authentication.

Turns out that if you want an MCP endpoint that agents can register against the way Claude Code, Codex, and the rest do, you need OAuth 2.1 plus DCR (dynamic client registration.) You also have to decide the scope bundles, host a consent page, deal with client registration, build a connected clients page, deal with consent in pre-authenticated and fresh authentication scenarios and more. And when I looked, OAuth 2.1 + DCR on Auth0 meant leaving the free tier for the Professional plan, around $250 a month, capped somewhere around 500 to 1000 monthly active users.

The $250 by itself wasn't the issue. The problem is how that kind of pricing scales for any SaaS. If you expect your user count to grow, you end up paying per user. Further, additional and more advanced features live further up the tiers. I didn't want to bake that into apps I might open source, and if I later wanted to host and sell them, it's real opex from day one.

So I went down a rabbit hole. I looked at Clerk and several other managed providers, not just Auth0. For what I needed, OAuth 2.1 + DCR with room to grow users, the economics came out broadly similar across the managed options: fine to start but a curve I didn't like as MAU scaled. The key blocker for me was paying the 250$ or so a month for low volume infrastructure to get more capabilities. So, I weighed the open-source options, and eventually settled on hosting Keycloak myself. I am currently using a single instance, always on Cloud Run instance (1 GiB, 1 vCPU) with a Supabase backend, Cloudflare doing DNS and proxy in front of the broker. That’s about 60$ for the infrastructure for now. At some point in the future, I plan to move all of the infrastructure onto a Hetzner host which would be cheaper & faster considering I might have a background worker, Redis, Keycloak and the app running on it + monitoring, proxying etc. ~100$ a month for everything. The cost/performance of that will be better because I don’t mind doing a small amount of operations.

Exploring down this rabbit hole took about two months of calendar time. The Keycloak repo and the Components repo are at the end of this post. If you want to do this, you should be able to clone it and adapt it. The app’s repo is separate and private at this time.

I want to be upfront about how the work actually got done, because it's part of the story. I used three AI tools, each in a specific role:

  • Claude on the web did the research, the specs, the brainstorming, and the errata log, all the thinking and writing about what to build and why.

  • Claude Code did the implementation at every turn, the SPIs, the theming, building out each fix.

  • Codex ran the security review passes.

The AIs wrote basically everything. The specs, the design docs, the errata, the code, the review notes.

My job was the other part: reading the outputs, questioning them, wiring it all together, testing, deploying, configuring, thinking about the end to end experience and deciding when it was wrong.

And to be honest: this was enormous fun! Hard, but fun.

The setup is three repos: 1) The app repo, 2) the Keycloak repo and 3) The components & theme repo. This decomposition is important because shoving everything into a single repository has led to multiple problems with consistency and debuggability issues in the past.

Quick orientation on why MCP auth is more than it sounds. MCP clients register themselves at runtime. You don't know them in advance, so you can't pre-configure them. They run on the user's own machine and redirect to loopback addresses. Each one has to go through a login that stops to ask which tenant the user is in and which scopes they're granting. That consent screen has to be my app's, not a generic identity-server page, because only my app knows the user's tenants. So the identity server has to pause a login mid-flight, hand off to my app, and then resume where it left off with the original client still attached. I run Keycloak federated-only: Google and Microsoft sign-in, no local passwords. Tenancy, permissions, and consent live in my app. I use Keycloak only as a thin broker.

The flow is something like this: When registering via MCP, the client retrieves MCP and auth configuration from the .well-known folder. Then the client talks to the auth endpoint to register itself. Keycloak starts the registration process, adds some claims, default scopes and audiences, and redirects to the app’s consent page. If a user is not already authenticated, then the login flow kicks in. Post-login, the MCP consent page in the app is shown. Post consent, the app registers the client and sends back the scopes that were actually authorized, along with initial claims back to Keycloak.

Remember: these are two separate web-apps with two separate deployments. So, the only way any state gets passed and rehydrated is through what is passed back and forth between the endpoints. On the redirect to Keycloak, Keycloak rehydrates the flow, registers the clients and hands back the auth token to the client. Now, the client is registered; when it talks to the app’s /mcp endpoint, it passes along the token which is then decoded and matched to the client + user + scope and then operations proceed normally.

New agents had nowhere to be marked as agents. A freshly registered client had no scopes and no login flow binding, so its first login couldn't be recognized as an MCP client and never hit the consent step. Fix: mark the client at registration time, inside the registration transaction, so it's either fully marked or not created at all.

The login flow won't compose the way the docs imply. Keycloak won't let you nest the authentication steps the naive way. A nested sub-flow only accepts certain settings, and I couldn’t configure the built-in container as needed. The answer was a different kind of sub-flow that the console does let you configure that way. I only found it by trial and error.

Keycloak bugs get you. At one point every client registration returned a 500 with no useful stack trace. The cause was a config list the admin UI had saved as [null], an array with a single null in it, which a URL matcher then tried to compile as a pattern. The UI that created it couldn't delete it. The fix was switching to the raw JSON editor and fixing that one executor’s configuration by hand.

Not round tripping original claims. After my app collected consent and signed a token back to Keycloak, the resume kept failing with "invalid client." I was sure it was a flaw in the resume logic. It wasn't that. Keycloak expected three correlation claims in that token that the app spec had missed. This needed a separate fix.

It worked for logged-in users and silently failed for everyone else. This is the one that got me. Everything passed, but only in a browser that already had a session. A fresh login went out to the selected identity provider, came back, and skipped consent entirely. This turns out to be documented, longstanding Keycloak behavior: steps placed after an external-identity redirect don't run for federated logins. The flow doesn't resume where it stopped. The fix was to host the same consent step a second time, in a different flow that does run after federated login and guard it so it stays dormant for anyone who isn't an agent.

I am not an identity expert. If I had to do this by hand, discovering, writing code, configuring settings, this might have taken months for an infrastructure/identity engineer to set up and run. The AI tools really helped me go faster on this. The specs gave me a plan, the implementation checked the plan against reality, and the reviews checked the result against an attacker. My job for two months wasn't writing. The tools write faster than I do and cover more ground. My job was to read what the tools produced, check it against reality, ask the question that exposed gaps, run it, watch it break, and feed that back in for the next round. There was a lot of judgement, steering, guiding, testing and questioning involved.

This is now built and locked down. Codex red-teamed it twice, and the findings are fixed. CodeQL and Dependabot are on and their issues are addressed. It's upgraded to the current Keycloak patch release with dependencies. There's one item left on my list, an access-control proxy in front of the admin endpoints using Cloudflare, and then it's done.

This took about two months of calendar time around a day job and a family. Not two months of full-time work but of course, a lot of late nights. I came out with an identity layer I own and run completely, that costs me about 50$ to run per month for now, and that also happens to let me deploy it in ways a SaaS vendor can't: an isolated cluster per customer, on-prem over a virtual connection, or fully air-gapped. For selling to enterprises, this might matter a lot. The apps I have built are also built the same way; minimal dependencies (app container + PostgreSQL + Redis) that will allow me to put them anywhere; any cloud provider or on-premises.

I have templatized my Keycloak repo with an example domain (yourapp.com). The repo is below. Clone it, adapt it, and you should be able to get this going without repeating the five turns above or spending the same amount of time. Hope this is useful for you. Enjoy.

IdP: https://github.com/priyavijaikalyan2007/idp
Theme: https://github.com/priyavijaikalyan2007/b2btheme

https://www.linkedin.com/pulse/mcp-via-keycloak-claude-codex-whole-lot-coffee-vijai-kalyan-g3nqf