AuralisDocs

How authentication works

Why the server checks the token audience, and why the scope is the second half.

/mcp accepts an OAuth 2.0 bearer token. Two things about that token are checked, and neither of them is which client asked for it.

Why the client identity is not checked

Ordinary applications register with the authorization server in advance, get a fixed client identifier, and the server can then pin that identifier.

MCP clients cannot work that way. A client registers itself, at the moment you add the server, through Dynamic Client Registration. It receives a fresh identifier nobody could have written down beforehand, and a different one next time. No static list could ever name it.

So the identifier is not what binds the token. The audience is.

Source: services/runtime/src/runtime/mcp/verifier.py:13-20 and services/runtime/src/runtime/config.py:524-535.

First half: the audience

The realm stamps a custom audience onto the tokens it issues. The server verifies the token's signature, its issuer and its validity window, and requires that audience to be present.

Source: services/runtime/src/runtime/mcp/verifier.py:120-124, with the required value read from services/runtime/src/runtime/config.py:535.

What that proves is narrow and worth stating plainly: the token was minted for this resource. It says nothing about which client asked for it, because the mapper that stamps the audience is a realm-level thing and applies to tokens broadly, self-registered clients included.

Source: services/runtime/src/runtime/mcp/verifier.py:22-30.

Second half: the scope

The second check is the granted scope. The verifier returns the token's real granted scopes, read from the token's own scope claim rather than assumed, and the transport requires the configured scope to be among them.

Source: services/runtime/src/runtime/mcp/verifier.py:136 for the scopes, and services/runtime/src/runtime/mcp/asgi.py:78-82 for the requirement.

The scope is set up in the realm as an optional client scope, not a default one. That distinction is the whole point. A default scope is added to every token the realm issues, so a client inherits it without asking. An optional scope is granted only when the client's own authorization request explicitly asks for it.

Put the two together: the audience says the token was minted for this resource, and the scope says the client deliberately asked to use it.

How a client knows the scope exists

It reads it. The protected resource metadata publishes the required scope as scopes_supported, and the MCP authentication specification has clients read that field before they send an authorization request.

Source: services/runtime/src/runtime/mcp/well_known.py:43-56, with the published value at services/runtime/src/runtime/mcp/well_known.py:55.

Omitting that field would leave the scope requirement enforced but undiscoverable: every compliant client would fail, because nothing would have told it the scope was there to ask for.

The two discovery documents

Both are public, and both carry identical content.

PathWho fetches it
/.well-known/oauth-protected-resource/mcpThe resource-scoped path an MCP client is expected to use, and the one named in the WWW-Authenticate header of a 401
/.well-known/oauth-protected-resourceThe same document at the origin's own well-known path, for a client that has not adopted the resource-scoped one

Source: services/runtime/src/runtime/mcp/well_known.py:1-21 and services/runtime/src/runtime/mcp/well_known.py:62-68.

Both documents are built once from configuration, never from the request's Host header. A metadata document asserts things about itself, so it must not take those assertions from something the caller supplies.

Source: services/runtime/src/runtime/mcp/well_known.py:18-20 and services/runtime/src/runtime/config.py:553-559.

What is checked once, and what is checked every time

Authentication happens once per connection. Everything about the workspace happens per call.

CheckWhen
Signature, issuer, validity window, audienceOnce, when the connection authenticates
Required scopeOnce, when the connection authenticates
Which workspace this call acts in, and membership of itEvery tool call
Role floor for this tool in this workspaceEvery tool call
MCP access not revoked hereEvery tool call

Source: services/runtime/src/runtime/mcp/verifier.py:32-50 for why no workspace is resolved at authentication time, and services/runtime/src/runtime/mcp/tools.py:132-166 for what happens on each call instead.

What this does not prove

Registration is anonymous. Any client that registers can ask for the scope and, if the person signing in has an account and a workspace, will get a working token. See Operator setup for the full statement of what this arrangement stops and what it does not.

On this page