# Connect your agents to Speakeasy

> Enterprise Managed Auth is in preview. Contact your Speakeasy account team to
> enable the Enterprise Managed Auth feature flags for your organization.

An agent that already holds a person's Okta identity can reach an MCP server without a browser sign-in. It asks Okta for an Identity Assertion JWT Authorization Grant (ID-JAG) addressed to the platform, and the platform's authorization server trades it for an access token. Okta decides which agents may act for which people; the platform trusts Okta as the issuer. This is the inbound half of [Enterprise Managed Auth](/docs/ai-control-plane/identity/identities/enterprise-managed-auth).

## Access requirements

> Trusting Okta requires the `org:admin` scope, which only the default [Admin
> role](/docs/ai-control-plane/org-admin/roles-and-permissions) holds. Choosing
> a server's user session issuer requires `project:write`. The Okta side needs
> an administrator who can configure Cross App Access.

## How it works

The agent starts from the person's Okta ID token and makes two exchanges: one at Okta for an ID-JAG, and one at the MCP server's authorization server for an access token. Neither step opens a browser.

Each MCP server whose [user session issuer](/docs/ai-control-plane/mcp-gateway/access/user-sessions) trusts Okta announces the JWT bearer grant and the ID-JAG profile in its authorization server metadata. Agents that support Enterprise Managed Authorization find it there.

## Setup at a glance

| Step                                                           | Where              | What                                                   |
| -------------------------------------------------------------- | ------------------ | ------------------------------------------------------ |
| [1. Connect Okta](#1-connect-okta)                             | Dashboard and Okta | The Okta connection, done once                         |
| [2. Trust Okta as an issuer](#2-trust-okta-as-an-issuer)       | Dashboard          | An organization-level sign-in issuer that trusts Okta  |
| [3. Use it on your MCP servers](#3-use-it-on-your-mcp-servers) | Dashboard          | Each server's **User session issuer**                  |
| [4. Register the agent](#4-register-the-agent)                 | Agent              | A client at the server's authorization server          |
| [5. Allow the agent in Okta](#5-allow-the-agent-in-okta)       | Okta               | Speakeasy as a Cross App Access resource for the agent |

## 1. Connect Okta

Connect your Okta organization on [**IDP and SSO > Identity providers**](https://ai.speakeasy.com/@self/identity?tab=identity-providers). The steps are the same as [Connect Okta](/docs/ai-control-plane/identity/identities/enterprise-managed-auth/services#1-connect-okta) on the services page. The connection adds your Okta organization, such as `https://example.okta.com`, as an authorization server the platform can trust.

## 2. Trust Okta as an issuer

A sign-in issuer accepts ID-JAGs when it is organization-level and trusts Okta. Project-level issuers never accept them.

1. Open **Manage Okta** and go to **Okta sign-in** on the **Setup** tab.
2. In **Sign-in issuers**, select **Trust Okta sign-in** on an existing organization-level issuer, or select **Set up Okta sign-in** to create the `okta-sign-in` issuer.
3. Open [**Identity > Authorization Servers**](https://ai.speakeasy.com/@self/projects/default/remote-identity-providers) in the project sidebar, select the Okta authorization server under **Organizational Authorization Servers**, and under **Issuer configuration** select **Refresh Discoverable Metadata**. The platform checks ID-JAG signatures against the key URL this reads from Okta.

## 3. Use it on your MCP servers

On each MCP server agents should reach, open **Settings > Sessions** and set **User session issuer** to the issuer from step 2.

The server's authorization server issuer is the URL Okta addresses ID-JAGs to. Read it from the `issuer` field of the server's metadata:

```bash
curl https://<mcp-host>/.well-known/oauth-authorization-server/mcp/<server-slug>
```

By default the issuer is the MCP server URL, `https:///mcp/`, and the token endpoint is that URL plus `/token`. A custom domain or a separate authentication host changes both, so always copy them from the metadata.

## 4. Register the agent

The agent needs a client at the server's authorization server. A client registered once works for every server that uses the same sign-in issuer.

- **Dynamic client registration.** Register at the `registration_endpoint` from the metadata and include `urn:ietf:params:oauth:grant-type:jwt-bearer` in `grant_types`. Redirect URIs are needed only if the agent also requests `authorization_code`.
- **Client ID metadata document.** Use the document's URL as the `client_id`, unless [client admission](/docs/ai-control-plane/mcp-gateway/access/client-registration) turns those off.

The agent authenticates at the token endpoint with the method it registered: `none`, `client_secret_basic`, `client_secret_post`, or `private_key_jwt`.

## 5. Allow the agent in Okta

Okta issues an ID-JAG only for a resource the agent has a connection to.

1. In Okta, open the app that represents Speakeasy, go to **Machine Assignments > Callers > Cross-app access (XAA) > Edit**, set it to **Enabled**, and enter the server's authorization server issuer from step 3 as the **Issuer URL**.
2. On the agent's AI agent registration, add a resource connection to that app. Use the MCP server URL as the **Resource** and the agent's client ID from step 4 as the **Client ID**.
3. Assign the people the agent acts for.

## What the agent sends

The agent first asks Okta for an ID-JAG with an OAuth token exchange:

```bash
curl https://example.okta.com/oauth2/v1/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
  -d requested_token_type=urn:ietf:params:oauth:token-type:id-jag \
  -d subject_token_type=urn:ietf:params:oauth:token-type:id_token \
  -d subject_token=<person's Okta ID token> \
  -d audience=https://<mcp-host>/mcp/<server-slug> \
  -d resource=https://<mcp-host>/mcp/<server-slug> \
  <agent's client authentication at Okta>
```

Then it presents the ID-JAG at the server's token endpoint:

```bash
curl https://<mcp-host>/mcp/<server-slug>/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
  -d assertion=<ID-JAG> \
  -d client_id=<agent's client ID> \
  <agent's client authentication, if any>
```

The platform accepts the ID-JAG only when all of these hold:

| Check        | Must be                                                                                                                                        |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `typ` header | `oauth-id-jag+jwt`                                                                                                                             |
| `iss`        | The Okta organization URL the sign-in issuer trusts, signed by its keys                                                                        |
| `aud`        | Exactly the server's authorization server issuer                                                                                               |
| `resource`   | The MCP server URL                                                                                                                             |
| `client_id`  | The client that authenticated at the token endpoint                                                                                            |
| `email`      | A member provisioned through [directory sync](/docs/ai-control-plane/org-admin/identity#how-directory-sync-works), matching exactly one person |
| `exp`, `iat` | Present, at most 10 minutes apart, with 1 minute of clock skew                                                                                 |
| `jti`        | Present and not used before. Each ID-JAG works once                                                                                            |

The person must also hold `mcp:connect` on the server. The access token lasts one hour and comes with no refresh token; the agent requests a new ID-JAG when it expires.

## Troubleshooting

Every refused ID-JAG returns `invalid_grant` with "assertion is invalid", so check these in order:

| Symptom                                                    | Likely cause                                                                                           |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Metadata lists no `authorization_grant_profiles_supported` | The server's user session issuer is project-level or doesn't trust Okta                                |
| Every ID-JAG is refused right after setup                  | The Okta authorization server has no key URL yet. Select **Refresh Discoverable Metadata**             |
| Refused for one server only                                | `aud` or `resource` names a different server, or the server uses another sign-in issuer                |
| Refused for one person only                                | Their email isn't on exactly one provisioned member, or they lack `mcp:connect`                        |
| `invalid_target`                                           | A `resource` parameter that isn't the MCP server URL                                                   |
| `invalid_client`                                           | The agent's client isn't registered at this authorization server, or authenticated with another method |

## Limitations

- Okta is the only supported issuer, and one sign-in issuer trusts one issuer.
- People are never created at sign-in. They must be provisioned first.
- ID-JAGs act for a person. For agents acting on their own, see [Agent identity](/docs/ai-control-plane/identity/agents).

---

Index of every Speakeasy page for agents: [llms.txt](https://www.speakeasy.com/llms.txt). AI control plane and MCP gateway content in one file: [llms-full.txt](https://www.speakeasy.com/llms-full.txt). Any page is available as markdown by appending `.md` to its URL or by requesting it with `Accept: text/markdown`.
