← Documentation

Agent Identity API

An identity per AI agent inside a customer's realm — how to register one, what its tokens carry, and what each of the two revocation paths actually stops.

RealmSSO can give an AI agent its own identity in your customer's realm instead of letting it share a human's session or a static API key. There are two separate mechanisms, they are not interchangeable, and the difference decides which revocation you get. All of it is API-only: there is no screen for any of it in the dashboard or the Admin Portal. See Agent & MCP Identity for the product view.

Agent clientAgent principal
What it isA confidential Keycloak client of its own, minted per named agentAn existing brokered user, flagged as acting agentically
How it authenticatesclient_credentials with its own client secretRFC 7523 JWT assertion signed by the customer's IdP
Which client it usesits own agent-… clientthe account's one org-<slug> client, alongside its humans
Gated byAPI authorization only — not the account switchThe account switch and the per-connection switch, both off by default
Revocation strengthImmediate for new tokensNew tokens only; issued tokens live out their lifetime

MCP is not part of this

Nothing on this page is MCP. RealmSSO publishes no RFC 9728 protected-resource metadata, implements no RFC 7591 dynamic client registration, performs no RFC 8693 token exchange, and brokers no outbound agent call. The registration endpoint below is RealmSSO's own API, not the RFC. MCP-shaped access is designed in ADR 0017, which is a proposal and is not built.

Agent clients

An agent client is for an agent that runs on your side and authenticates for itself. Registering one provisions a new confidential client in that account's own Keycloak realm, with its service account enabled, and attributes that service account as an agent.

Register an agent client

POST/api/v1/agent-clients
FieldTypeNotes
accountIdstringRequired
namestring (1–255)Required — appears in the generated client id
ownerstring (1–255)Required. Who is accountable for this agent; becomes the agent_owner claim. Refused if blank, because it is what makes revocation actionable
scopesstring[]Optional optional-client-scopes. Omitting it grants none at all — it never defaults to everything

Authenticate with a vendor session or an X-API-Key. A key needs the agent-clients:write scope, and the caller needs at least member on the account. The generated client id is agent-<account-slug>-<agent-slug>-<random>.

{
  "data": {
    "id": "…",
    "accountId": "…",
    "name": "nightly-reconciler",
    "owner": "platform-team@example.com",
    "status": "active",
    "keycloakClientId": "agent-acme-nightly-reconciler-9f3c21ab",
    "clientId": "agent-acme-nightly-reconciler-9f3c21ab",
    "clientSecret": "…"
  }
}

The secret is returned exactly once

Nothing persists it — the AgentClient row has no secret column, the same rule API key creation follows. If it is lost, register a new agent client and revoke the old one.

What the registration policy fixes, and what it refuses

The client is created with a deliberately narrow shape, and none of it is caller-controlled:

  • Confidential and client_credentials-only. The standard flow, the implicit flow and direct access grants are all disabled and redirectUris is empty, so there is no browser-facing path into it.
  • fullScopeAllowed is always false, and registration assigns the service account no realm or client role at all.
  • offline_access is refused outright, whatever the realm's own configuration says — so the credential cannot be traded for a standing refresh token.
  • A requested scope that the realm does not already define, or that is on that deny list, is rejected with a validation error and nothing is provisioned. It is not silently dropped, so a caller who asked for something they cannot have is told at registration rather than discovering it in production.

Registration is deliberately not idempotent: each call mints a new credential, which is what stops a retry after an ambiguous timeout from silently rotating an earlier agent's secret. Uniqueness of a logical agent name is enforced separately, by a unique constraint on (accountId, name). And if any step after the client is created fails — reading back the service account, writing the agent attribution, ensuring the claim mappers — the just-created Keycloak client is deleted before the error is returned, because a client that exists with no agent attribution is worse than no client at all: nothing downstream would know to distrust it.

List, read, revoke

GET/api/v1/agent-clients?accountId=…
GET/api/v1/agent-clients/:id
POST/api/v1/agent-clients/:id/revoke

The two reads need viewer on the account and the agent-clients:read scope; revoke needs member and agent-clients:write. Revoke is a POST and not a DELETE on purpose, and it is idempotent — revoking an already-revoked client returns the row unchanged.

The four claims

Every token minted for an agent carries four normalized claims, and a human's token carries none of them. The mappers are stock Keycloak user-attribute mappers with no conditional logic: Keycloak omits a user-attribute claim entirely when the attribute is unset, which is what makes “never on humans” true without anything to get wrong per request.

ClaimJSON typeMeaning
agentbooleanAlways true when present. Typed as a boolean so your application can check token.agent === true rather than the string
agent_sourcestringWhich trust path admitted the agent. Constrained to entra, okta, scim, local, dcr
agent_ownerstringWho is accountable for the agent, as asserted at registration or marking
agent_parentstringUpstream parent id, for attribution and revocation. Optional

An agent client registered through the endpoint above carries agent_source: "dcr", and its agent_parent is its own client id — because disabling that client is how the agent is revoked, and nothing further upstream is known at registration time.

Access token and userinfo — not the ID token

The mappers set access.token.claim and userinfo.token.claim to true and id.token.claim to false. The ID token is the artifact most likely to be logged, cached, or handed to a browser, so the agent attribution deliberately stays out of it. No introspection claim is set either. Validate the claims on the access token, or fetch userinfo.

Two limits worth knowing before you build on this. The claim names are a convention ratified in ADR 0003, and no SDK validates them for you — checking them is your application's job. And the claims only reach a token because the four attributes are declared in the realm's User Profile; RealmSSO declares them when it ensures the mappers, but an attribute written to a realm that has not had that done is dropped by Keycloak silently, mappers and all.

Agent principals and the RFC 7523 grant

The other mechanism is for an agent the customer's identity provider vouches for. It authenticates non-interactively with a JWT that provider signed, using the RFC 7523 JWT Authorization Grant, and it is off by default at two separate grains.

The account switch

POST/api/v1/accounts/:id/agent-access

Body is { "enabled": boolean }. It is off on every account until someone turns it on. This route is vendor-session only — the accounts module keeps its routes in a sub-plugin that API keys cannot reach at all, so an API key cannot flip an account's agent policy no matter what scopes it holds.

The switch is asymmetric, and the difference matters operationally

Enabling touches no connection. It lifts the gate and nothing else; each connection still has to turn its own grant on.

Disabling cascades. It walks every connection on the account that currently holds the grant and revokes each one in Keycloak. That is a write per connection and it can partly fail: the account flag still flips off — an off switch one broken connection can veto is not an off switch — but the request answers 502 AGENT_ACCESS_REVOKE_INCOMPLETE and names every connection still live-granted, and each of those keeps assertionGrantEnabled: true because Keycloak genuinely still holds its grant. Re-POST enabled: false to retry; the cascade re-selects exactly the connections still granted, so it converges.

The connection switch

POST/api/v1/connections/:id/assertion-grant

Body is { "enabled": boolean }. Needs member and, for a key, connections:write. Enabling is refused in four cases, each of them a real constraint rather than a validation formality:

  • The connection is not provisioned in Keycloak yet, so there is no identity provider to enable it on.
  • The connection is SAML. RFC 7523 needs a JWT bearer assertion from the customer's IdP, and a SAML IdP has no token endpoint and no JWKS to produce one — OIDC connections only.
  • The account's agent-access policy is off. Turn that on first.
  • The account's SSO client currently carries offline_access. An agent principal authenticates against that same client — this product provisions exactly one per account — so enabling the grant would silently widen what an agent could hold. Turn offline access off first (see Connections API).

Disabling is never blocked by any of the gates above. Turning a protection off must always be possible.

Marking an agent principal

POST/api/v1/connections/:id/agent-principals
FieldTypeNotes
userIdstringRequired — the Keycloak internal user id of a user already brokered through this connection
sourceenumRequired — one of entra, okta, scim, local, dcr
ownerstringRequired — becomes agent_owner
parentstringOptional — becomes agent_parent

This is an explicit, authenticated action against a user who already exists in the realm, not an automatic pipeline: nothing infers an agent from a directory sync today. It is refused on a SAML connection, refused while the connection's assertion grant is off (the claims would be ones no token could ever carry), and it verifies the user really is federated through this connection's identity provider — so one connection's endpoint cannot mint agent claims for a user who never came through it.

Revocation: two paths, two different strengths

These are not the same operation with two names, and a reader who treats them as equivalent will mis-handle an incident. RealmSSO states the difference rather than letting “revoked” imply the stronger property on both.

Revoke an agent clientRevoke an agent principal
EndpointPOST /api/v1/agent-clients/:id/revokePOST /api/v1/connections/:id/agent-principals/:userId/revoke
MechanismThe Keycloak client is disabledThe four claim attributes are deleted from the user
New tokensNone. A disabled client cannot complete client_credentials at allStill minted — they simply carry none of the agent claims
Tokens already issuedUnaffected; they expire normallyUnaffected; they keep agent: true until they expire
SessionsNot applicable — an agent client has no user sessionNot terminated. No user is logged out
Effective immediately?For new tokens, yesNo. The exposure window is the realm's access-token lifetime, and the response quotes it

Revoking an agent client disables rather than deletes it, so the client and its agent attribution stay inspectable for audit afterwards.

Revoking an agent principal returns the exposure alongside the result, because ADR 0004 makes stating the window a requirement of any surface offering the verb — silence would be read as “immediate”:

{
  "data": {
    "userId": "…",
    "clearedClaims": ["agent", "agent_source", "agent_owner", "agent_parent"],
    "wasMarked": true,
    "exposure": {
      "accessTokenLifespanSeconds": 300,
      "accessTokenLifespanMinutes": 5,
      "sessionsTerminated": false,
      "issuedTokensInvalidated": false,
      "statement": "This is not immediate. No token minted from now on carries the agent claims, but access tokens already issued keep them, and stay valid, for up to 300 second(s) (about 5 minute(s)), this realm's access-token lifetime. Sessions are not terminated and the user is not logged out — ending sessions is a separate, explicit operation (ADR 0004)."
    }
  }
}

accessTokenLifespanSeconds is null when Keycloak could not be read, and the statement says so rather than substituting a plausible default. Revoking a principal is deliberately not gated on the connection being OIDC or on the assertion grant still being enabled: a principal marked while the grant was on has to stay revocable after it is turned off.

What is not built

Stated here because a rollout plan needs it before you commit, and because the parts that are absent are the ones most likely to be assumed present:

  • Everything MCP. No protected-resource metadata, no standards-based dynamic client registration, no token exchange, no brokering of what an agent calls outward. ADR 0017 describes how it would work in both directions and is a proposal.
  • Per-agent authorization for principals. An agent client gets a registration-time scope policy. An agent principal authenticates against the account's single org-<slug> client alongside its humans, so there is no per-agent scope or role split for one today.
  • The agent as an audit actor. The audit log records the person or API key that acted on an agent — agent_client.registered, agent_client.revoked, connection.agent_principal_marked, agent_principal.revoked — never the agent itself as the actor. See Audit Logging.
  • Automatic agent detection. An agent principal is flagged by an explicit API call naming a Keycloak user. Nothing infers one from SCIM or from a directory.
  • Any user interface. Every endpoint on this page is API-only.

Related

Access Control for the API key scopes named here, Connections API for the connection those grants sit on, and Agent & MCP Identity for the product view.