Agent Identity API
How to give an AI agent its own identity in a customer's realm, and how to stop it.
RealmSSO can give an AI agent its own identity in your customer's realm. A realm is the separate sign-in space that RealmSSO keeps for each account. The agent then does not share a human's session or a static API key. There are two mechanisms, and they are not interchangeable. The difference decides how you can stop the agent. The dashboard has a page for most of this, and the API does all of it.
See Agent & MCP Identity for the product view.
| Property | Agent client | Agent principal |
|---|---|---|
| What it is | A confidential client of its own, minted per named agent | An existing user who signed in through a connection, marked as an agent |
| How it authenticates | client_credentials, with a client secret or with a JWT its platform minted (no secret) | RFC 7523 JWT assertion signed by the customer's identity provider (IdP) |
| Which client it uses | its own agent-… client | the account's one org-<slug> client, with its humans |
| Gated by | API authorization and the account switch | The account switch and the connection switch, both off by default |
| How to stop it | Revoke the client. New tokens stop at once | Turn off the connection’s assertion grant. On the RFC 7523 path, this stops every principal on it |
What is MCP-shaped today, and what is not
Live now: GET /api/v1/sso/accounts/:id/config returns the realm as an authorization server. It also returns an RFC 9728 protected-resource document for your own MCP server. See MCP discovery for your MCP server.
Building now: RFC 7591 dynamic client registration, RFC 8693 token exchange between RealmSSO's own services, per-agent scopes and an egress broker. The registration endpoint below is RealmSSO's own API, not RFC 7591. See Building now.
Agent clients
An agent client is for an agent that runs on your side and authenticates for itself. Registering one makes a new confidential client in that account's own realm. The client's service account is marked as an agent.
Register an agent client
/api/v1/agent-clients| Field | Type | Notes |
|---|---|---|
| accountId | string | Required |
| name | string (1–255) | Required. It appears in the generated client id |
| owner | string (1–255) | Required. Who is accountable for this agent. It becomes the agent_owner claim, and a blank value is refused |
| scopes | string[] | Optional client scopes to add, each already defined in the realm. If you omit it, the client gets no optional scope. It never defaults to all scopes |
| federation | object | Optional. Omit it for a client secret. Send { issuerType, issuer, subject } for a client with no secret (below) |
Authenticate with a dashboard session or an X-API-Key. A key needs the agent-clients:write scope. The caller needs at least member on the account. The generated client id is agent-<account-slug>-<agent-slug>-<random>.
The account must already have its realm and its org-<slug> SSO client. One call to POST /api/v1/sso/accounts/:id/client creates both. Otherwise registration is refused with 400, and nothing is created.
With a client secret
Omit federation. The response carries clientSecret, and authMethod is client_secret.
{
"data": {
"id": "…",
"accountId": "…",
"name": "nightly-reconciler",
"owner": "platform-team@example.com",
"status": "active",
"authMethod": "client_secret",
"keycloakClientId": "agent-acme-nightly-reconciler-9f3c21ab",
"clientId": "agent-acme-nightly-reconciler-9f3c21ab",
"clientSecret": "…"
}
}Status 201. Abridged: the response carries every AgentClient field except federatedBindingKey, including grantedScopes.
The secret is returned exactly once
RealmSSO's database does not keep it: the AgentClient row has no secret column. The sign-in service holds it, and no route returns it again. If you lose it, register a new agent client. Then revoke the old one.
With no secret: federated agent clients
An agent that runs on a platform with its own OIDC issuer can hold no secret at all. The agent presents a JWT that the platform minted for it. RealmSSO calls this a federated agent client, and its authMethod is federated_jwt. No secret exists — not in the response, not in RealmSSO's database and not in the sign-in service. The sign-in service refuses any client secret presented for it.
| issuerType | The platform | subject | Token reuse |
|---|---|---|---|
kubernetes | A Kubernetes service-account issuer with public discovery, such as a managed EKS, GKE or AKS cluster | system:serviceaccount:<namespace>:<name> | A projected token can be used again, until one hour after it was issued |
oidc | Any other platform issuer. GitHub Actions is the named case | repo:<org>/<repo>:ref:refs/heads/main | Each assertion is single-use. Present it within five minutes of issue |
RealmSSO designed this for public issuers such as these. Not yet tested against GitHub Actions or a managed Kubernetes cluster: the tests used a stand-in issuer shaped like them.
{
"accountId": "…",
"name": "nightly-reconciler",
"owner": "platform-team@example.com",
"federation": {
"issuerType": "oidc",
"issuer": "…",
"subject": "repo:acme/reconciler:ref:refs/heads/main"
}
}The response carries no secret. It tells the agent what to send instead:
{
"data": {
"id": "…",
"name": "nightly-reconciler",
"status": "active",
"authMethod": "federated_jwt",
"federatedIssuerType": "oidc",
"federatedIssuer": "…",
"federatedSubject": "repo:acme/reconciler:ref:refs/heads/main",
"clientId": "agent-acme-nightly-reconciler-9f3c21ab",
"federation": {
"issuerType": "oidc",
"issuer": "…",
"subject": "repo:acme/reconciler:ref:refs/heads/main",
"audience": "…",
"tokenEndpoint": "…",
"clientAssertionType": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
}
}
}Status 201. Abridged: the response carries every AgentClient field except federatedBindingKey, including grantedScopes.
The agent asks its platform for a token whose aud is federation.audience. It then posts to federation.tokenEndpoint with grant_type=client_credentials. It sends client_assertion_type as given, and the platform's token as client_assertion. The audience is the account's realm issuer. It is the same value /config returns as authorizationServer.issuer.
RealmSSO refuses a federated registration in these cases. A refusal creates no client. Under a race, a 409 can leave an unused issuer entry in the realm. That entry cannot finish a browser login.
- The issuer, or the key URL it publishes, is not
https:(400). - The issuer, or the
jwks_uriits discovery document names, is on a private address (400). RealmSSO applies the same outbound URL guard as it does to a connection's discovery URL. So an in-cluster issuer, such askubernetes.default.svc, is refused. That is the guard working. - The issuer's discovery document names a different issuer, or advertises no
jwks_uri(400). - The issuer has a query, a fragment or credentials, or a kubernetes issuer ends in
/(400). - The subject has surrounding whitespace or is over 512 characters (
400). - A kubernetes subject is not
system:serviceaccount:<namespace>:<name>(400). - The
federationobject has a field the schema does not name (400). - The realm already trusts an
oidcissuer with a different key URL (409). Moving the keys of every agent that trusts an issuer is a platform operator's change. - The realm already trusts the issuer as the other
issuerType, or through an identity provider RealmSSO did not create (409). - Another active client already binds the same
(issuer, subject)(409). Under a race, at most one registration succeeds, and all can fail. So retry a first409once. - Per-account hostnames are switched on, and the account has no recorded SSO origin yet (
409). Its first connection or SSO client would move the issuer. Give the account its hostname first. - The account shares its realm with another account (
400). The platform issuer would then be shared with the other account. - RealmSSO cannot fetch the issuer's discovery document (
502).
No human can sign in through a platform issuer. The realm never offers it on the login page, and no browser login can finish through it. An integration test shows this on the real sign-in service, with an upstream that would sign anyone in.
What the registration policy fixes, and what it refuses
RealmSSO creates the client with a narrow shape. The caller cannot change any of it:
- Confidential, and
client_credentialsonly. The standard flow, the implicit flow anddirect access grantsare off.redirectUrisis empty, so no browser path leads into it. fullScopeAllowedis alwaysfalse. Registration gives the service account no roles in its tokens.offline_accessis refused, whatever the realm's own configuration says. So the client cannot get a standing refresh token.- A requested scope that the realm does not define, or that is on that deny list, is rejected. RealmSSO returns a validation error and provisions nothing. It does not drop the scope silently.
Registration is not idempotent. It never returns or rotates an existing client. Each name can be registered once per account. A second registration with the same name is refused with 409, even after a revoke. So after a timeout, a same-name retry returns 409 if the first call succeeded. Then revoke that client, and register under a new name.
If a step fails after the client exists, RealmSSO deletes the new client. A client with no agent attribution is worse than no client. Nothing downstream could attribute its actions to an agent.
List, read, revoke
/api/v1/agent-clients?accountId=…/api/v1/agent-clients/:id/api/v1/agent-clients/:id/revokeThe two reads need viewer on the account and the agent-clients:read scope. They never return a secret. Revoke needs member and agent-clients:write. Revoke is a POST, not a DELETE, and it is idempotent.
For a federated client, revoke also releases its (issuer, subject). Revoke, then register again under a new name: that is how you rotate a federated agent. A revoked client keeps its name.
Hostname moves and federated agents
A platform operator can move an account's SSO hostname in a coordinated window. The move changes the realm issuer. So it also changes the audience that every federated agent of the account must request. The move plan lists each active federated agent whose audience changes, with the old and the new audience. The move refuses to apply until the platform operator passes --acknowledge-federated-agents.
Before you pass it, arrange each agent's re-issue for the same window. The sign-in service refuses the new audience until the realm moves. So each agent's platform re-issues its tokens right after the apply. Until then, that agent cannot authenticate.
The four claims
An agent client's own tokens carry attribution claims. For a marked agent principal, only tokens that the account's own client issues carry them. A human's token carries none of them. The mappers are standard user-attribute mappers with no conditions. The sign-in service omits a user-attribute claim when the attribute is not set. That is what keeps the claims off human tokens.
A marked principal that signs in to a product client (product-<slug>) or a mobile client (mobile-<slug>) gets no agent claims. We reasoned this from where the mappers sit. We did not measure it.
| Claim | JSON type | Meaning |
|---|---|---|
| agent | boolean | Always true when present. Use it to label and log the caller, never to decide access |
| agent_source | string | Which trust path admitted the agent: entra, okta, scim, local or dcr. Only agent clients carry dcr |
| agent_owner | string | Who is accountable for the agent, as given at registration or marking |
| agent_parent | string | Upstream parent id, for attribution. Optional |
An agent client registered through the endpoint above carries agent_source: "dcr". Its agent_parent is its own client id, because nothing further upstream is known at registration.
Attribution, not authorization
The claims say who acted. They never say what the agent may do. Read them to label, log and audit the caller. Do not allow or refuse a request because of them.
The mappers put the claims in the access token and the userinfo response. They keep them out of the ID token, which is the artifact most likely to be logged or cached. The mappers set no introspection flag of their own.
Two limits apply. No SDK reads the claims for you. And a claim reaches a token only if its attribute is declared in the realm's User Profile. RealmSSO declares the four attributes when it ensures the mappers. In a realm without that declaration, the sign-in service drops the attribute silently.
Agent principals and the RFC 7523 assertion grant
The other mechanism is for an agent that the customer's identity provider vouches for. It signs in without a browser, with a JWT that provider signed. It uses the RFC 7523 assertion grant. The assertion grant is off by default at two separate grains.
The account switch
/api/v1/accounts/:id/agent-accessThe body is { "enabled": boolean }. The switch is off on every account until someone turns it on. It needs member. This route is dashboard-session only, and an API key gets 401. The accounts module keeps it in a sub-plugin that API keys cannot reach. The switch also covers agent clients. While it is off, registering an agent client, with a secret or with none, returns 400 and creates nothing. Turning it off disables every existing agent client in the sign-in service, so none can get a new token. Turning it back on re-enables those clients, but not one that was revoked. Tokens already issued stay valid until they expire.
The switch is asymmetric
Enabling turns on no connection's grant. Each connection must still turn its own assertion grant on. Enabling does re-enable each agent client that turning the switch off disabled, but not one that was revoked in between.
Disabling cascades. It turns off the assertion grant in the sign-in service on every connection that holds it, and disables every agent client of the account that is not revoked. That is one write per connection or client, and a write can fail. The account flag flips off in any case. The request then answers 502 AGENT_ACCESS_REVOKE_INCOMPLETE. It names each connection that still has its assertion grant on, and each of those keeps assertionGrantEnabled: true, because the sign-in service still holds its assertion grant. It also lists, in stillEnabledAgentClients, each agent client not confirmed disabled, and it can answer for agent clients alone. Treat each listed client as still able to get new tokens.
To retry, send enabled: false again. The cascade selects only the connections that still have their assertion grant on and the agent clients not yet disabled, so it converges.
Re-enabling can fail too. If a re-enable is not confirmed, the request answers 502 AGENT_ACCESS_RESTORE_INCOMPLETE. The flag is already on, and stillDisabledAgentClients lists each agent client not confirmed re-enabled. That includes the case where the list of clients to re-enable could not be read; then none is re-enabled. To retry, send enabled: true again. It selects exactly those clients, so it converges.
A 200 carries disabledAgentClientIds when the switch turns off, and reEnabledAgentClientIds when it turns on.
A busy account answers 409 IDP_STATE_BUSY. The request changes nothing, so you can send it again.
The connection switch
/api/v1/connections/:id/assertion-grantThe body is { "enabled": boolean }. It needs member and, for a key, connections:write. RealmSSO refuses the request in these cases:
- The connection is not provisioned in the sign-in service yet. It has no identity provider to change.
- The connection is SAML. RFC 7523 needs a JWT from the customer's IdP, and a SAML IdP cannot produce one. OIDC connections only.
- You enable it, and the account's agent access is off. Turn that on first.
- You enable it, and the account's SSO client carries
offline_access. An agent principal uses that same client, so the assertion grant would widen what an agent could hold. Turn offline access off first (see Connections API).
The first two checks apply to every request. The last two apply only when you enable, so turning the assertion grant off is never blocked by them.
A save onto a different issuer deletes every user's link
Editing a connection's provider settings re-provisions its identity provider in the realm, and RealmSSO then re-applies the connection's assertion grant. A save that keeps the same issuer updates the identity provider in place, so every user's link survives and its agent principals keep working.
A save that changes the issuer replaces the identity provider and deletes every user's link to it. An agent principal is then refused with "User not found", and nothing re-links the user. A returning employee is stuck at "Account already exists".
A test on the real sign-in service shows both.
Marking an agent principal
/api/v1/connections/:id/agent-principals| Field | Type | Notes |
|---|---|---|
| userId | string | Required. The user id, in the realm, of a user who already signed in through this connection |
| source | enum | Required. One of entra, okta, scim, local. The route refuses dcr |
| owner | string | Required. It becomes agent_owner |
| parent | string | Optional. It becomes agent_parent |
Marking is an explicit, authenticated action on a user who already exists in the realm. Nothing marks an agent from a directory sync. RealmSSO refuses to mark on a SAML connection, or while the connection's assertion grant is off. It also checks that the user came through this connection's identity provider. The route refuses dcr as a source, because only agent clients carry it.
It needs member and, for a key, connections:write. Un-marking needs the same. A userId that is not in the realm gets 502, not 404.
Stopping an agent
The two mechanisms stop in different ways, and neither recalls a token already issued. A reader who treats them as the same will mishandle an incident.
| Property | Revoke an agent client | Un-mark an agent principal |
|---|---|---|
| Endpoint | POST /api/v1/agent-clients/:id/revoke | POST /api/v1/connections/:id/agent-principals/:userId/revoke |
| What it does | Disables the client in the sign-in service | Deletes the four claim attributes from the user |
| New tokens | None. A disabled client gets no token from client_credentials | Still minted, as a human’s token with no agent claims |
| Does it stop the agent? | Yes, for new tokens | No. It changes attribution only |
| Tokens already issued | Not recalled. A signature-only check accepts them until they expire | They keep agent: true until they expire |
| Sessions | Not applicable. An agent client has no user session | Not ended. No user is logged out |
Revoking an agent client disables it and does not delete it. So the client and its attribution stay available for audit.
To stop an agent principal on the RFC 7523 path today, turn off its connection's assertion grant, which stops every principal on that connection, or disable the user. A disabled user's fresh assertion is refused. RealmSSO disables a user only through SCIM (active: false, with automatic provisioning on). A dedicated stop for one principal is not built yet. The route's path says revoke, but it un-marks. It returns the window in which issued tokens still carry the claims:
{
"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."
}
}
}accessTokenLifespanSeconds is null when RealmSSO cannot read the sign-in service. The statement then says so and does not guess a default. The example shortens the statement text. Un-marking works on any connection, OIDC or not, and with the assertion grant on or off. A principal marked while the assertion grant was on must stay un-markable after it is turned off.
The dashboard
The dashboard's Agent Identity page uses the routes on this page. It needs viewer to read and member to change. On it you can:
- List, view and revoke agent clients.
- Register an agent client with a secret, which the page shows once, or with no secret (a federated agent client).
- Turn the account's agent access on and off. Off refuses new agent clients and disables the existing ones, and the page marks each client the switch disabled. See each connection that a partial cascade left with its assertion grant on.
- Turn a connection's assertion grant on and off.
- Mark and un-mark agent principals, and read their history from the audit log.
MCP discovery for your MCP server
An MCP server is an OAuth 2.1 resource server. An MCP client finds its authorization server through the server's RFC 9728 protected-resource document. To put an MCP server in front of one customer's realm, read the account's integration config:
/api/v1/sso/accounts/:id/config{
"data": {
"authorizationServer": {
"issuer": "…",
"metadataUrl": "…"
},
"protectedResourceMetadataTemplate": {
"document": {
"resource": null,
"authorization_servers": ["…"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["openid", "profile", "email"]
},
"vendorMustSet": ["resource"],
"wellKnownPath": "/.well-known/oauth-protected-resource"
}
}
}Abridged to the two discovery fields. The response also carries account, hostname, oidc, connections and realm.
Set resource to your MCP server's own URL, and serve the document at wellKnownPath on your server. RealmSSO leaves resource as null, because it cannot know your URL. RealmSSO does not serve this document itself. scopes_supported adds offline_access only when the account's offline-access policy allows it.
Your MCP server still validates each token and decides each call. This template makes discovery work. It adds no scope and no per-tool control. The route needs viewer and, for a key, connections:read.
The audit trail
Actions on an agent are recorded with the person or API key that acted: agent_client.registered, agent_client.revoked, connection.agent_principal_marked and agent_principal.revoked.
When an agent client authenticates, the audit log records the agent itself as the actor. actorType is agent, and actorId is the agent client's client id. The action is agent.authenticated or agent.authentication_failed. RealmSSO reads these from the sign-in service's events, every 60 seconds by default. So a record can arrive up to a minute late. It reads revoked clients too.
Two limits apply. The agent actor covers agent clients only, not agent principals. And RealmSSO cannot see what an agent does with its token at your application. See Audit Logging.
Building now
We are building these now. None of them is available yet, and no route on this page does any of this yet.
- Scopes and tools for agents. In the design, you would name the scopes of your resource. A platform operator would register the resource. An account
adminwould grant the scopes to an agent. The realm is designed to refuse to mint a scope that was not granted. Your server must still check each scope. - Egress destinations and egress grants. In the design, an agent would call a third-party MCP server or API through RealmSSO's egress broker. The agent would never hold the credential. An egress grant would name the agent client, the destination and the allowed scopes or tools. It would also set how long each credential it issues lives, and an optional budget. The default would be deny, and every decision would be audited.
- Standard registration and token exchange. In the design, an RFC 7591 registration policy would sit behind the existing route. A control-plane realm would offer RFC 8693 token exchange between RealmSSO's own services.
What is not built, and known defects
A rollout plan needs these before you commit. The absent parts are the ones most likely to be assumed.
- Per-agent scopes. An agent client gets a registration-time scope policy. An agent principal uses the account's single
org-<slug>client with its humans. So a principal has no scope split today. - A dedicated stop for one principal. Un-marking changes attribution only. Today the stops on the RFC 7523 path are the connection's assertion grant, which stops every principal on that connection, and disabling the user, which RealmSSO does only through SCIM.
- Standards-based MCP registration and delegation. No RFC 7591, no RFC 8693, and no egress broker.
- Automatic agent detection. An agent principal is marked by an explicit API call. Nothing infers one from SCIM or from a directory.
- Known defects. A save onto a different issuer deletes every user's link to the connection's identity provider, and nothing re-links them. The
409for a revoked client's name says to revoke it first. But that name can never be registered again.
Related
See Access Control for the API key scopes. See Connections API for the connections that hold the assertion grants. See Agent & MCP Identity for the product view.