Your MCP Server Is an OAuth Resource Server Now: Okta as the AS, and the Resource-Indicator MUST Everyone Skips
π‘ Key Concept
The June 2025 MCP authorization revision settled an argument the ecosystem had been having all year: an MCP server is not its own identity provider. It is an OAuth 2.1 resource server, full stop. It accepts bearer access tokens, validates them, and delegates every question of "who is this and what may they do" to an external authorization server. That single decision is what lets an enterprise put its AI agents' tool access under the same identity fabric β the same policies, MFA, and audit trail β as its human workforce. It is also exactly the half of the "agents connecting to tools" story that today's AI pill does not cover: that pill is about fitting tools into the context budget; this one is about who is allowed to call them.
The wiring runs on two RFCs most people skim past. Discovery uses Protected Resource Metadata (RFC 9728): the MCP server publishes a .well-known/oauth-protected-resource document naming which authorization server(s) it trusts, and an unauthenticated call gets a 401 whose WWW-Authenticate header points the client at that metadata. Binding uses Resource Indicators (RFC 8707): the client MUST send resource= the canonical URI of the target server on both the authorization and token requests, so the AS mints a token whose audience is that one server and nothing else.
This is where Okta steps in as the authorization server. A custom authorization server issues the audience-scoped tokens, API Access Management evaluates least-privilege dynamically on identity, context, and risk, and β announced for GA on April 30, 2026 β Okta for AI Agents registers each agent as a first-class non-human identity with a human owner, while its Agent Gateway acts as a virtual MCP server that aggregates tools and logs every agent-to-resource call for audit. The spec gives you the protocol; Okta gives you the governance plane on top of it.
π¬ Deep Dive
- The handshake is a 401 that carries a map, not an error. A compliant MCP client never guesses where to authenticate. It calls the server cold, reads the
WWW-Authenticateheader on the 401, fetches the Protected Resource Metadata it points to, and only then knows which authorization server to talk to. Publish that metadata wrong β omit theresourceidentifier, or list anauthorization_serversentry that does not exactly match your Okta issuer β and every client silently fails discovery before a token is ever requested.# 1) Agent calls the tool with no token. The 401 is the routing signal. $ curl -i https://mcp.acme.com/mcp HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://mcp.acme.com/.well-known/oauth-protected-resource" # 2) The Protected Resource Metadata (RFC 9728) the client then fetches: { "resource": "https://mcp.acme.com/mcp", "authorization_servers": ["https://acme.okta.com/oauth2/aus7ph1l0sMcpTools"], "scopes_supported": ["mcp:tools.read", "mcp:tools.invoke"], "bearer_methods_supported": ["header"] } - Practitioner trap β the confused deputy is a missing audience check, and the spec bans token passthrough for exactly this reason. Two bugs produce the same breach. First, if the client omits
resource=, the AS may mint a broadly-scoped token, and a token accepted atmcp.acme.comis then replayable atmcp-partner.comβ a classic confused deputy. Second, if your MCP server does not verify that the token'saudequals its own canonical URI, it will happily accept a token minted for someone else. The canonical-URI match is byte-exact: a trailing slash or a mixed-case host makes RFC 8707 validation miss, and you either reject valid callers or β worse β accept the wrong audience. The spec is blunt that an MCP server MUST NOT accept a token that was not issued for it and MUST NOT forward the caller's token upstream to another API (token passthrough); it exchanges for a new, correctly-scoped token instead.# The two-line check that stops the confused deputy. Validate BEFORE you trust. CANONICAL = "https://mcp.acme.com/mcp" # must match RFC 8707 resource, byte-for-byte claims = verify_jwt(token, jwks=OKTA_JWKS, issuer=OKTA_ISSUER) aud = claims["aud"] if isinstance(claims["aud"], list) else [claims["aud"]] if CANONICAL not in aud: raise Reject(401, "token audience is not this server") # someone else's token # The pen test: mint a token WITHOUT resource=, try it at a second server. # -> accepted at both == confused deputy, enforce aud today # -> rejected at MCP-B == audience binding is working - Okta is the authorization server, and Agent Gateway is where the audit trail lives. In Okta you model each MCP server's permissions as scopes on a custom authorization server (
mcp:tools.invoke, one scope-set per tool group), and an access policy grants them per agent identity with dynamic conditions on risk and context. The strategic piece announced for the April 30, 2026 GA is that agents are registered as non-human identities in Universal Directory, and the Agent Gateway can front your tools as a virtual MCP server so that every agent-to-tool call is brokered and logged in one place β turning "which agent called which tool with whose consent" from an unanswerable question into a queryable log. - Staff framing β every MCP server is a new audience, and unmanaged that is authorization sprawl with a fresh acronym. Ten MCP servers is ten resource servers, ten audiences, and ten independent bearer-token validators, each written by whichever team shipped the tool. Left alone, that is the microservice-auth problem all over again: inconsistent validation, tokens over-scoped because nobody wanted to file a scope request, and no single place to revoke an agent that has gone wrong. The architect's deliverable is not "MCP servers require a token" β it is a registry mapping every agent identity to its human owner, every token to a single audience, and every scope to a decommission path, all fronted by one authorization server so revocation is one action rather than ten. That inventory is the blast-radius control, and it is what a board asks for the first time an agent does something it should not have been able to do.
π§ Recall
From the Jul 13 pill on Okta CIBA: when an agent needs a human to approve a high-risk action, what does the CIBA flow give you that stuffing an "ask the user" step into the agent's prompt does not?
Show answer
CIBA decouples the approval from the agent's execution channel: the authorization request is delivered out-of-band to the human's own authenticator, and the token is only issued after they consent β so approval is cryptographically bound and logged by Okta, not a text string the agent could hallucinate, summarize away, or be prompt-injected into skipping. It pairs with today's topic: CIBA answers who approved this action; resource indicators answer which single server this token may touch. An agent needs both β a correctly-scoped token for an action nobody approved is still a governance hole.
πΌ Market Signal
Per Okta's Q1 FY2027 results (reported May 28, 2026), revenue was $765M, up 11% YoY, and on the earnings call management noted new products β led by Okta Identity Governance β made up roughly 25% of Q1 bookings, with deals including those products carrying a ~40% average-contract-value uplift over standalone access management. CEO Todd McKinnon framed AI agents as "the fastest-growing identity in the enterprise" and the least governed. That is the demand curve under this pill: the buyer already has the budget line, and the thing they cannot yet buy off the shelf is someone who can wire MCP authorization into their existing Okta tenant.
The comp backdrop for the role: IAM Architect base averages $194,807 with a 75th percentile of ~$246k (Salary.com, June 2026). But the pricing signal that matters for a fractional practice is scarcity, not the median β "secure our agents' tool access with Okta as the AS" is a sentence almost no in-house team can execute today, which is precisely the shape of work that gets an outside architect a retainer instead of a ticket.
β‘ Action This Week
Prove the confused deputy to yourself. Stand up two trivial MCP servers (or two protected endpoints), point both at one Okta custom authorization server, and run the binding test: request a token without resource= and show it is accepted by both servers; then add the resource= indicator plus the aud check from the Deep Dive, and show the token minted for server A is now rejected by server B.
Definition of done: two terminal captures β one where a single token authenticates against both servers (the vulnerability), and one where audience binding refuses the cross-server replay while the correctly-scoped call still succeeds.
This is a strong LinkedIn post because almost no one has connected the dots publicly: "Everyone is racing to give agents MCP tools. Here's the one OAuth field that decides whether one stolen agent token is a breach or a shrug." Ship the fix in the same post so it reads as guidance, not FUD.