Noizu.MCP.Auth.JWTVerifier (Noizu MCP v0.1.6)

Copy Markdown View Source

Audience-checking JWT verifier for an MCP mount.

The one thing Noizu.MCP.Auth.CompoundJWTVerifier does not do is bind the token to this resource. This verifier does, and that binding is the point: a token minted for https://host/mcp is rejected at https://host/mcp/learning and vice versa, so a mount cannot be used as a confused deputy for its neighbour.

forward "/mcp/learning", Noizu.MCP.Transport.StreamableHTTP.Plug,
  server: MyApp.Learning.MCP,
  auth: [
    verifier: {Noizu.MCP.Auth.JWTVerifier, [
      resource: "https://app.example.com/mcp/learning",
      issuer: "https://app.example.com",
      secret: {MyApp.MCPAuth, :secret},
      algorithms: ["HS256"],
      scopes: ["mcp"]
    ]},
    resource_metadata: :derive
  ]

Options

  • :resource (required) — the canonical resource URI of this mount. The token's aud must match it exactly (see below).
  • :secret — HMAC secret: a binary, {module, function}, or a 0-arity fun resolved per request (so a release reads it from runtime config, not from a compile-time literal).
  • :jwk — an asymmetric key, as a JOSE.JWK or a JWK map. Use instead of :secret for RS256/ES256.
  • :algorithms — allowed JWS algorithms, default ["HS256"]. Always read from config, never from the token header — that is what makes alg: "none" and HS/RS confusion non-issues here.
  • :issuer — required iss claim, when set.
  • :scopes — scopes every token must carry, else {:error, :insufficient_scope, %{"scope" => ...}} (→ 403 step-up).
  • :leeway — clock skew allowance in seconds for exp/nbf, default 0.
  • :subject_required — reject a token with no sub, default true. An MCP tool handler that authorizes on ctx.assigns.auth_claims["sub"] silently authorizes nobody when sub is missing.

Audience matching

aud must be either the configured resource as a string, or a single element list holding it. A multi-audience token is rejected: it is valid at more than one resource, which is exactly the property audience binding exists to remove. Our own authorization server mints single-audience tokens, so this costs nothing and closes the case where an upstream IdP is talked into adding an audience.

Summary

Functions

The scopes a token carries, as a list. Handy in tool handlers reading ctx.assigns.auth_claims.

Functions

scopes(claims)

@spec scopes(map()) :: [String.t()]

The scopes a token carries, as a list. Handy in tool handlers reading ctx.assigns.auth_claims.