Your app is the other half of agent SSO
Okta's Agent SSO went GA on the Cross App Access standard. The IdP brokers the connection, but your app still has to validate the grant and issue the token.
Okta made Agent SSO generally available on August 24, folding the Cross App Access standard into core Okta SSO plans at no additional cost. It reads like identity provider news. The spec underneath it says otherwise: the IdP brokers the connection, but it never issues the token your API accepts. That half is yours to build.

Agent SSO answers exactly one question
The framing in Okta's own release is narrow: "Agent SSO answers one question: how do Cross App Access agents connect to enterprise applications and MCP servers?" It applies to AI agents that already support Cross App Access, and the prebuilt integrations named are Anthropic (Claude), Archestra.AI, Asana, Atlassian, Canva, Datadog, Figma, Glean, Granola, Linear, MintMCP, Notion, Slack, and Supabase.
Most of those aren't agents. Asana, Linear, Notion, Slack, Supabase, Datadog, Figma: those are the applications agents want to reach. Being on that list means implementing the resource side of the protocol — accepting an IdP-issued grant and minting your own access token from it.
Bundling Agent SSO into every core SSO plan doesn't do that work for anyone else. Agent SSO is free in core SSO plans, and the broader Okta for AI Agents product is a separate subscription; neither line item lands on your side of the connection. What the bundling does is put "does your product support this?" into routine security review for a customer base Okta puts at more than 20,000.
The spec keeps you as the token issuer
Cross App Access is the friendly name for an IETF draft: the Identity Assertion JWT Authorization Grant, ID-JAG for short, currently at draft-04 and last updated 2026-05-21. It profiles two existing specs: OAuth 2.0 Token Exchange (RFC 8693) for the first hop, and the JWT Profile for OAuth 2.0 Authorization Grants (RFC 7523) for the second.
The draft defines four roles, not two: the Client, the IdP Authorization Server, the Resource Authorization Server, and the Resource Server. Agent SSO is a product for the second one. If you sell software an agent connects into, you are the third and the fourth.
That split is deliberate. From the introduction:
“this specification preserves the Resource Authorization Server as the issuer of access tokens for its protected resources.”
And the consequence, from the same paragraph:
“The Resource Authorization Server determines whether to honor a given ID-JAG, what scopes or authorization details to grant, and what access token to issue, applying its own local policy.”
The IdP's assertion is an input to your authorization decision, not a replacement for it. An admin can say in their console that Claude may act as Priya in your product with these scopes. Your authorization server still decides whether Priya exists on your side, what she can reach, and how long the resulting token lives.
What actually lands on your token endpoint
The client does the token exchange at the IdP first, asking for requested_token_type=urn:ietf:params:oauth:token-type:id-jag with an audience identifying your authorization server. What comes back is not an access token. The exchange response sets token_type to N_A, and the draft's example gives it a 300-second lifetime.
Then it arrives at your door as a JWT bearer grant. This is the draft's own access token request example:
POST /oauth2/token HTTP/1.1
Host: acme.chat.example
Authorization: Basic yZS1yYW5kb20tc2VjcmV0v3JOkF0XG5Qx2
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJIUzI1NiIsI...Decoded, the assertion looks like the draft's non-normative example:
{
"typ": "oauth-id-jag+jwt"
}
.
{
"jti": "9e43f81b64a33f20116179",
"iss": "https://acme.idp.example",
"sub": "U019488227",
"aud": "https://acme.chat.example/",
"client_id": "f53f191f9311af35",
"exp": 1311281970,
"iat": 1311280970,
"resource": "https://acme.chat.example/api",
"scope": "chat.read chat.history",
"auth_time": 1311280970,
"amr": [
"mfa",
"phrh",
"hwk",
"user"
]
}
.
signatureiss, sub, aud, client_id, jti, exp, and iat are required. resource, scope, authorization_details, act, tenant, auth_time, acr, amr, aud_tenant, aud_sub, sub_id, and email are optional.
Four checks are not negotiable. The typ header must be oauth-id-jag+jwt. The signature validates against the IdP's JWKS endpoint. The aud claim must contain your authorization server's own issuer identifier; if it doesn't, you reject with invalid_grant, which the draft calls out specifically as preventing audience injection. And the client_id claim must identify the same client as the client authentication on the request, or it's invalid_grant again.
After that you're back in ordinary OAuth territory, with one wrinkle: the scopes you grant may be a subset of what the IdP approved, because local policy is still yours. The authorization_details claim carries Rich Authorization Requests (RFC 9396) if you want structure beyond scope strings.
Subject resolution is where the real work is
Validating a JWT is an afternoon. Deciding which of your users the JWT is about is the part that touches your data model.
The assertion's sub lives in the IdP's namespace, not yours. The spec recommends including email and/or aud_sub so you can resolve the subject, including just-in-time provisioning for a user who has never signed into your product. MCP's Enterprise-Managed Authorization docs give the same instruction in operational terms:
“Use the subject claim as the primary stable identifier for the user, and fall back to the email claim for matching against pre-existing accounts that were created before enterprise-managed authorization was configured.”
If you resolve users by SAML NameID instead, there's a saml-nameid Subject Identifier Format for the sub_id claim:
"sub_id": {
"format": "saml-nameid",
"issuer": "https://idp.example.com/exk33qwjcwDda7luK346",
"nameid": "foo@example.com",
"nameid_format": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"sp_name_qualifier": "https://resource.example.com/saml/sp"
}You must not resolve the subject on nameid alone unless local policy explicitly defines nameid as the subject identifier for that SAML issuer. An email-shaped nameid from an issuer you didn't mean to trust is exactly the kind of thing that looks fine in a demo.
Then multiply by issuers. A deployment may trust more than one IdP, but for each one you have to recognize the issuer, resolve its identities to the right local principal, and associate the grant with the correct client relationship. That's per-IdP configuration living in your product, not a checkbox in someone else's console.
No refresh token, and what that costs you
The draft says your authorization server should not return a refresh token in this flow. When the access token expires, the client may re-present the original ID-JAG; if the ID-JAG has expired too, it goes back to the IdP for a new one.
For a chat client with a human watching, fine. For a long-running agent, every renewal path now runs through the IdP's policy engine. That's the point, but it's also a dependency worth testing in its failure states. Work out what your product does when the IdP is slow, when policy revokes mid-task, and when an agent is holding a still-valid access token minted from an assertion that has since been withdrawn.
A central kill switch only moves as fast as your shortest token lifetime: if your access tokens live an hour, an hour is the widest gap between a revoked grant and an agent losing access. Pick that default on purpose.
The resource-app column is the short one
oauth.net's Cross-App Access page tracks implementations by role. Okta, Ping, Athenz, Keycloak, and Descope appear as IdPs. Claude, Visual Studio Code, WorkOS, and Archestra appear as clients, alongside Auth0, Authplane, Athenz, Descope, Doorkeeper, Keycloak, Ping, Scalekit, and Stytch as authorization servers (several of them still marked beta or in progress). The resource apps: Asana, Atlassian, Canva, Datadog, Figma, Granola, Linear, Notion, Slack, Supabase.
Ten, against every SaaS product an enterprise agent might reasonably be pointed at. Meanwhile only 34% of organizations apply the same security controls to AI agents as they do to human workers, per Okta's AI Agents at Work 2026 report, and MCP extensions are opt-in and never active by default. The distance between "the standard exists" and "your customer's agent can use it against your API" is implementation work on the app side, all of it.
What this means if you build the app
Cross App Access is open. It extends OAuth, it's a working-group draft with authors from Okta, Ping Identity, and an independent contributor, and it's the official Enterprise-Managed Authorization extension for MCP, registered as io.modelcontextprotocol/enterprise-managed-authorization. It isn't limited to AI agents either. It covers any case where one application acts for a user, like syncing meeting notes from one tool into another.
So the decision in front of most software teams isn't which IdP to buy. It's whether an enterprise admin can point an agent at your product and have it work without a per-user consent screen and a long-lived API key.
AuthKit already accepts ID-JAG assertions from enterprise IdPs — in early access today — and handles the pieces described above: JWKS signature verification, audience and claims validation, policy enforcement, and issuing the scoped access token. Cross App Access entered MCP as one of the first authorization extensions alongside the 2025-11-25 spec, via SEP-990, and we walked through the full flow when it did — then again when Claude shipped enterprise-managed auth for MCP connectors, the same week as Okta's news. How an agent gets the client relationship all of this rides on — the client_id your token endpoint authenticates — is a story of its own.