mcpresource validator. It covers which signing algorithms the validator accepts, scope guards, step-up authentication, structured denial bodies, securitySchemes metadata for tools/list, and URL mode elicitation. For the validator itself, see Resource Server.
All code on this page uses the github.com/glincker/theauth-go/mcpresource module and the standard library only.
Signing algorithms and audience
The validator accepts access tokens signed withEdDSA, ES256 or RS256. The list is fixed. none and HS* are always rejected, and there is no option to widen it.
The key type must match the header
alg. A token that claims RS256 but resolves to an Ed25519 key is rejected, as is a JWK that pins a different alg.
The aud claim may be a string or an array. The token is accepted when the array contains the resource URI passed to mcpresource.New. After validation, Principal.Audience holds that resource URI and Principal.Audiences holds every value the token carried.
Wire the validator
v.Middleware authenticates the token and attaches the Principal. RequireScope and RequireStepUp read that principal, so they must run after Middleware. A request that reaches either guard with no principal is answered 401 invalid_token.
Scope guards
RequireScope(scopes...) admits a request only when the token carries every listed scope. Scopes are compared as exact strings.
A principal that lacks a scope gets 403:
scope parameter lists the scopes the route needs, so a client can request exactly those on re-authorization. A token bound with DPoP is answered with the DPoP scheme instead of Bearer.
When all tools share one JSON-RPC endpoint, wrap each tool handler and dispatch by tool name. Authentication still runs once, in v.Middleware, in front of the dispatcher.
Step-up authentication
Some tools need a stronger or fresher authentication than the one the user did to get the token.RequireStepUp implements the resource server side of RFC 9470.
When the token does not satisfy the policy, the response is 401:
acr_values and max_age, then retry with the new token.
A missing acr or auth_time claim fails a requirement that depends on it. The resource server only reads these claims. The authorization server has to put them in the access token.
Check the policy yourself
StepUp.Satisfied(p, now) is the same check as a plain method. Use it when the decision depends on the request body, for example only transfers above a threshold need MFA:
Structured denials
Every 401 and 403 that the validator and the guards write carries a JSON body next to theWWW-Authenticate header. The header is the normative signal. The body lets a client or an agent branch on the same facts without parsing it.
WriteDenial(w, status, d) writes the body with Content-Type: application/json and Cache-Control: no-store. It does not set WWW-Authenticate. Set that header before you call it, as in the example above.
Tool security metadata in tools/list
MCP clients that follow the OpenAI Apps SDK convention read asecuritySchemes list on each tool to decide whether to start authorization before calling it. ToolSecurity and PublicToolSecurity build that metadata, and Apply writes it into a tool descriptor map.
read_file the descriptor gains the list at the top level and again under _meta. Other _meta members already on the tool are kept.
ping gets [{"type": "noauth"}]. ToolSecurity() with no scopes declares oauth2 with no scopes, meaning any valid token. SecuritySchemes(scopes...) returns just the list, with duplicates and empty strings dropped, if you build the descriptor by another route.
This metadata is advisory. It describes what the tool expects. Enforcement stays in RequireScope and RequireStepUp, so keep the two in step.
URL elicitation
When a tool cannot proceed until the user does something in a browser, for example authorizing a downstream service, the server can ask the client to open a URL.NewURLElicitation builds the request and URLElicitationRequiredError wraps it in the JSON-RPC error with code -32042.
NewURLElicitation returns an error when the message is empty, the URL is not absolute, the URL embeds credentials, or the scheme is not https. http is accepted only for a loopback host such as localhost. Do not put tokens or other secrets in the URL: clients show it to the user and may log it.
Read authorization details and authentication context
AfterMiddleware, the principal carries the authentication context claims and any RFC 9396 authorization_details:
AuthorizationDetails() returns a copy. A nil result means the token carries none. For tokens that trigger introspection (an act chain or a delegation grant), details missing from the JWT are taken from the introspection response. A malformed value, or an element without type, fails the request with 401 invalid_token.
Limits
max_age=0cannot be expressed. A zeroMaxAgemeans no age requirement, not “always re-authenticate”. Use a small positive value such astime.Second.- Only three algorithms.
EdDSA,ES256(P-256) andRS256are accepted.PS256,ES384,RS384andRS512tokens are rejected, and the allowlist is not configurable. - Opaque tokens are not authenticated. The validator verifies JWT access tokens (RFC 9068). A token that is not a three segment JWT is rejected. Introspection is used to walk delegation chains, not to accept opaque tokens.
securitySchemesis advisory. Clients may ignore it, and nothing checks that it matches your guards.- The authorization server decides
acrandauth_time. The resource server cannot raise authentication strength. If your issuer does not put these claims in access tokens, a step-up policy that depends on them can never be satisfied. - No certification claims. These controls implement the named RFC behaviors. They are not a statement of conformance or compliance with any program.
Next steps
- Resource Server (mcpresource) - Validator options and the
Principaltype. - Sender-Constrained Tokens (DPoP) - Bind tokens to a client key.
- Agent delegation - Produce tokens with an actor chain.
- MCP Authorization - How the pieces fit together.