Skip to content
← Documentation

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.

PropertyAgent clientAgent principal
What it isA confidential client of its own, minted per named agentAn existing user who signed in through a connection, marked as an agent
How it authenticatesclient_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 usesits own agent-… clientthe account's one org-<slug> client, with its humans
Gated byAPI authorization and the account switchThe account switch and the connection switch, both off by default
How to stop itRevoke the client. New tokens stop at onceTurn 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

POST/api/v1/agent-clients
FieldTypeNotes
accountIdstringRequired
namestring (1–255)Required. It appears in the generated client id
ownerstring (1–255)Required. Who is accountable for this agent. It becomes the agent_owner claim, and a blank value is refused
scopesstring[]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
federationobjectOptional. 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.

issuerTypeThe platformsubjectToken reuse
kubernetesA Kubernetes service-account issuer with public discovery, such as a managed EKS, GKE or AKS clustersystem:serviceaccount:<namespace>:<name>A projected token can be used again, until one hour after it was issued
oidcAny other platform issuer. GitHub Actions is the named caserepo:<org>/<repo>:ref:refs/heads/mainEach 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_uri its 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 as kubernetes.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 federation object has a field the schema does not name (400).
  • The realm already trusts an oidc issuer 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 first 409 once.
  • 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_credentials only. The standard flow, the implicit flow and direct access grants are off. redirectUris is empty, so no browser path leads into it.
  • fullScopeAllowed is always false. Registration gives the service account no roles in its tokens.
  • offline_access is 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

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. 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.

ClaimJSON typeMeaning
agentbooleanAlways true when present. Use it to label and log the caller, never to decide access
agent_sourcestringWhich trust path admitted the agent: entra, okta, scim, local or dcr. Only agent clients carry dcr
agent_ownerstringWho is accountable for the agent, as given at registration or marking
agent_parentstringUpstream 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

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

The 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

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

The 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

POST/api/v1/connections/:id/agent-principals
FieldTypeNotes
userIdstringRequired. The user id, in the realm, of a user who already signed in through this connection
sourceenumRequired. One of entra, okta, scim, local. The route refuses dcr
ownerstringRequired. It becomes agent_owner
parentstringOptional. 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.

PropertyRevoke an agent clientUn-mark an agent principal
EndpointPOST /api/v1/agent-clients/:id/revokePOST /api/v1/connections/:id/agent-principals/:userId/revoke
What it doesDisables the client in the sign-in serviceDeletes the four claim attributes from the user
New tokensNone. A disabled client gets no token from client_credentialsStill minted, as a human’s token with no agent claims
Does it stop the agent?Yes, for new tokensNo. It changes attribution only
Tokens already issuedNot recalled. A signature-only check accepts them until they expireThey keep agent: true until they expire
SessionsNot applicable. An agent client has no user sessionNot 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:

GET/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 admin would 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 409 for 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.