Agents in the Open Part 2: Sign-In Without Handing Out Admin

Part 1 limited what the Orrery Lab can reach and change. This part is about who gets in. The obvious answer was "people with a Tirraflow account", using the Keycloak realm the blog already has. It...

Part 1 limited what the Orrery Lab can reach and change. This part is about who gets in. The obvious answer was "people with a Tirraflow account", using the Keycloak realm the blog already has. It was also the dangerous one, for a reason that applies to any two applications sharing an identity provider.


The Trap: A Role Name Means Nothing Outside Its App

Orrery reads roles from the token and maps them to its own three: viewer, operator, admin. Which claim it reads is configuration: JWT_ROLE_CLAIM.

The blog's admins hold a role called admin. If the lab read the realm's or the blog client's roles, every blog admin would arrive in Orrery as an Orrery admin. And "admin" would then mean two different things, decided by whoever edits the blog's roles, for reasons that have nothing to do with the agent.

The rule I followed: an application reads only the roles issued for it. A token can carry roles for many clients, and each app must look only at its own.


Two Clients, One Audience

The lab has two Keycloak clients:

How a Tirraflow sign-in becomes an Orrery role: composite roles add the lab-api roles, the token gets the lab-api audience, and Orrery reads only that client's roles

Why two? Keycloak's built-in audience mapper adds a client to a token's aud when the user holds one of that client's roles, but never the client the user signed in to. So the roles live on a second client, and Orrery checks for it:

JWT_ISSUER=https://tirraflow.com/auth/realms/scribe
JWT_AUDIENCE=orrery-lab-api
JWT_ROLE_CLAIM=resource_access.orrery-lab-api.roles

Orrery checks the signature against Keycloak's keys, fetched over the internal network, then the issuer and the audience. Only then does it read roles, and only from that one client.


Admins Only, Decided by the Token

The first version is for Tirraflow admins only, and that is still the case today: if you sign in with a reader account, the lab answers 401. That's expected, not a bug. The gate is a Keycloak composite role: orrery-lab-api's role lab-user is included in the blog's admin role. Only an admin's token gets the lab audience. Everyone else, whether they signed in with a password, GitHub or Google, gets 401 on every call.

I first considered a rule in Keycloak's login flow instead. It wouldn't hold: social logins finish outside the browser flow, so a GitHub user would have slipped past it. A check on the token, in the application, covers every way of signing in.

Inside the lab, roles stay separate too. lab-user isn't an Orrery role name, so on its own it means viewer. Admins also get orrery-admin, which unlocks the Agents view and the catalog editor. Even then autonomy is L2, so an Orrery admin can't change any system through a tool. They can rewire the lab's agents (switch specialists off, add agents with their own instructions), and those edits last until the lab restarts. Opening the lab to every reader later means one change: grant lab-user to the realm's default roles. They will arrive as viewers.

One side effect showed up on the first sign-in: a 401, then 200s. Roles are baked into a token when it is issued, so a session from before the role existed had no lab audience. Signing out and back in fixed it. Any role change behaves this way, so tell users to sign in again.


Configuration at Runtime, Checked at Startup

The console's sign-in settings used to be compiled into the web bundle. The published image could only show a "paste a token" box. Orrery now serves them at runtime from /console-config.js, built from environment variables, so one signed image works for every deployment.

Moving configuration to runtime moves the mistakes there too. Orrery also refuses to start if the console's issuer isn't https (except on localhost), or if it isn't the issuer the API checks tokens against. Otherwise the console would get tokens from one server while the API expected another, and every request would fail with 401 and no clear cause.


Changing Keycloak Without Breaking It

Creating the clients is a script, keycloak/lab-client.sh. It is safe to re-run, and it reads the admin password from stdin so the password never appears in a command line. Two lessons from running it:


The Pattern

With access settled, the next question is about the agent itself. When someone asks the lab a question, how does the coordinator decide which specialist answers it, and why can't a disabled specialist sneak back in? That's Part 3.

Read on Tirraflow