Repository navigation
v0.8.0
neva 0.8.0 lets a handler see who is calling, gates tools, prompts and resources on OAuth scopes with a step-up the neva client already follows, and validates an authorization server's tokens when the server cannot reach the issuer's URL. It is a minor bump for narrow source breaks and two stricter token checks; see Migration. MSRV is unchanged at 1.90.
Who is calling
claims() on a handler's Context and on a middleware's MwContext: the claims of the caller's access token, as the HTTP engine validated it, or None without one (#161).
#[tool]
async fn whoami(ctx: Context) -> Result<String, Error> {
let Some(caller) = ctx.claims() else {
return Err(Error::new(ErrorCode::InvalidRequest, "no authenticated caller"));
};
// A subject is unique only within its issuer: key per-user data by both.
Ok(format!(
"{} from {}",
caller.subject().unwrap_or("nobody"),
caller.issuer().unwrap_or("no issuer")
))
}Claimsgainsissuer(),audience(),client_id()andscopes()(RFC 9068), whichDefaultClaimsreads fromiss,aud,client_idandscope.downcast_ref::<T>()ondyn Claimsgives an engine's own claims type back.claims()compiles withouthttp-serverand answersNonethere, so handler code builds the same on a stdio-only feature set.neva::auth::{Claims, DefaultClaims}need onlyserver.
Scope-gated tools, prompts and resources
scopes = [..] on #[tool], #[prompt] and #[resource], and with_scopes(..) on Tool, Prompt, ResourceTemplate and UiResource. Every listed scope is required (#162).
#[tool(scopes = ["mcp:tools", "mcp:admin"])]
async fn purge_cache() -> &'static str {
"cache purged"
}- A token short of a scope gets a
403withWWW-Authenticate: Bearer error="insufficient_scope", scope=".."(RFC 6750 section 3.1), andErrorCode::InsufficientScope(-31403) withrequiredScopesandmissingScopesindata. The neva client re-authorizes for the union of what it holds and what the challenge names, and retries (SEP-2350). - The check runs before dispatch, so a streamed reply and a batch are refused with a
403as well; a batch is refused whole. Both protocol profiles. scopes_supportedin the Protected Resource Metadata defaults to the declared scopes;with_scopesonwith_oauth_metadataoverrides it.- Scopes are checked after
rolesandpermissions, which keep their200+InvalidParamsrefusal. A scope-gated item stays in the listings: a client has to see it before it can step up for it.
Keys for an issuer the server cannot reach
In OAuth issuer mode one URL is the discovery endpoint, the iss tokens must carry, and where clients are sent to authorize. A server in a container, or behind a public ingress, reaches the issuer at another address than the one tokens and clients use (#163).
http.with_auth(|auth| auth.with_oauth(|oauth| oauth
// what tokens carry in `iss`, and where clients go
.with_issuer("http://localhost:8787")
// where this server, in a container, gets the keys
.with_jwks_uri("http://host.docker.internal:8787/jwks")
.with_config(|cfg| cfg.with_client_config(|c| c.require_https(false)))))OAuthConfig::with_jwks_urifetches the key set from that URL and skips discovery. Rotation, refresh cooldown and key age work as with discovery.AuthConfig::with_decoding_keytakes a static RSA, EC or Ed25519 public key, from PEM or a JWK, for a server that cannot reach the issuer at all. Pair it withwith_alg,with_issandwith_aud.neva::auth::DecodingKeyis re-exported. A static key does not follow rotations.- A server with both a static key and
with_oauthfails to start. - The Volga adapter requires volga 0.13.3, which makes static RSA, EC and Ed25519 keys validate tokens at all (RomanEmreis/volga#280), and has
with_issrequire the claim (RomanEmreis/volga#281).
Migration
DefaultClaims.aud is a Vec<String>, read from a string or an array; it was Option<String>. DefaultClaims has two new public fields, client_id and scope, so a struct literal needs them or ..Default::default(). Prefer caller.audience() and caller.scopes() to the fields.
ErrorCode has a new variant, InsufficientScope. An exhaustive match on it needs an arm.
with_alg replaces the default HS256 instead of adding to it, so with_alg(Algorithm::HS512) no longer accepts HS256 tokens; to accept both, name both. A static key whose family does not match the configured algorithms fails the start, an RSA or EC key without with_alg included.
with_iss refuses a token without iss. It compared the claim only when the token had one. To accept such tokens, leave with_iss out.
Behaviour changes
- A middleware's own answer, without calling
nextor after a timeout around it, reaches the caller. It was dropped, and the caller waited out its timeout (#164). - A
requestStateis bound to the caller's issuer as well as its subject. One minted before the upgrade for an authenticated caller is refused once, as a principal mismatch. scopes_supportedis no longer empty whenwith_scopeswas not called and the server declares scopes.- Under
legacy-spec, a reply carryingInsufficientScopeis sent with403, where every dispatched reply was200.
Fixed
- A token whose
audis an array is accepted.jsonwebtokenvalidated it, andDefaultClaimsthen failed to decode it, so a valid token got a401(#161). - With
tracingon and no subscriber installed, start failures (runtime, transport, HTTP engine) and the MCP Apps resource warning went nowhere, and a misconfigured server exited without a word. They go to stderr now.
The full list is in CHANGELOG.md. Diff: 0.7.1...0.8.0.