v0.2.0
Released commit: 6c81739453cb1d3937ef52d0de2d5c4c8a50d894
Added
core/resource/verifier:(*VerifiedClaims).RequireScopes(scopes ...string) errorplural helper. Returnsnilon empty input, and on failure wrapsErrInsufficientScopenaming every missing scope plus the scopes the token does carry, so an adapter can surface it verbatim in theWWW-Authenticateerror_description.core/resource/verifier:ErrMultipleDpopProofssentinel for RFC 9449 §4.3 #1 violations.core/resource.AuthErrorResponsemaps it to aDPoP error="invalid_dpop_proof"401 challenge per RFC 9449 §7.1.core/resource/verifier:NewDPoPContext(method, url, dpopHeaderValues []string) (*DPoPContext, error)factory — the canonical §4.3 #1 enforcement boundary. Filters blanks, splits on,defensively for proxies that pre-join duplicate headers, and returnsErrMultipleDpopProofson more than one non-blank value.core/resource/verifier:(*DPoPContext).Proof()nil-safe accessor returning the single proof (or""when none).mark3labsmodule: adapter formark3labs/mcp-go. Wraps*authplanehttp.Adapterso consumers get bearer + DPoP auth, RFC 9728 PRM, andHTTPContextFuncintegration for the mark3labs HTTP server.HTTPContextFunctakesWithForwardedContextKeys(keys ...any)andWithContextForwarding(fn)options to propagate values (request IDs, tracing spans, …) from the upstream request context onto the per-tool-call MCP context. Seemark3labs/docs/user-guide.md.core/resource:Resource.PRMURL()returns the precomputed absolute Protected Resource Metadata URL as a single infallible source of truth.core/resource:Resource.PRMConfig()returns the Protected Resource Metadata as a typedPRMConfigstruct, for feeding into third-party PRM-serving handlers (e.g. mark3labs/mcp-go'sNewProtectedResourceMetadataHandler) without going through the dynamicPRMResponsemap.core/authplane:TokenResponse/IntrospectionResponseexpose the RFC 9449 §6 confirmation —Cnf json.RawMessage(rawcnf, verbatim) andCnfJkt string(DPoP thumbprint fromcnf.jkt, empty when unbound). Derived at parse time and preserved acrossTokenCachehits.
Fixed
core/resource/verifier:validateHTUcomparesEscapedPath()instead ofPath, so an encoded%2Fis no longer treated as equivalent to a literal/. The previous comparison conflated distinct request targets, weakening the RFC 9449 §4.3htubinding (RFC 3986 §6.2.2.2 only permits decoding unreserved characters when comparing URLs).core/resource/verifier:validateHTUstrips an explicit default port (:80for http,:443for https) on both sides before comparing (RFC 9110 §7.2), so a resource configured ashttp://api.example.com:80/mcpno longer mismatches every client that signs the port-less form.core/resource/verifier:validateHTUcollapses an empty path to/on both sides before comparing, so a client signing a bare-origin htu (e.g.https://host) no longer fails against a server where every inboundr.URL.EscapedPath()is at least/.core/internal/cache:TokenCache.Setdistinguishes a missingexpires_infromexpires_in: 0(RFC 6749 §5.1) — nil applies the default TTL,0is refused as born-expired instead of cached for the default hour.core/internal/dpop:NormalizePathupper-cases percent-encoded hex triplets (RFC 3986 §6.2.2.1) on both comparison sides, so a proof signed with%2fmatches a request reconstructed with%2F.
Changed
- BREAKING (pre-1.0)
core/authplane:TokenResponse.ExpiresInchanges fromint64to*int64, so a response that omitsexpires_inis nownilrather than0. This distinguishes an absent field from an explicitexpires_in: 0(RFC 6749 §5.1 permits a deliberately-expired one-shot token) and aligns the Go shape with the optionalexpires_inwire contract (absent vs.0vs. positive). Migration: any caller readingresp.ExpiresIndirectly (arithmetic, comparison, formatting) must dereference and nil-check; treatnilas "apply your default" and*v == 0as "already expired". - BREAKING (pre-1.0)
http: DPoPhtureconstruction in thenet/httpadapter no longer reads the inboundHostheader,r.TLS, orr.URL.RawQuery. BothHostandr.TLSare proxy-controlled and would otherwise let a misconfigured edge — or an attacker forgingHost— shift thehtubinding to a different origin or downgradehttpstohttpbehind a TLS-terminating proxy. The adapter now sources scheme + authority from the operator-configured resource URI and contributes only the request path in rawEscapedPathform (query and fragment are dropped per RFC 9449 §4.3 #5). Migration: mount the middleware before anyhttp.StripPrefixsor.URL.EscapedPath()still reflects the path the client signed; apps relying onHost-derived htu (orr.TLS-derived scheme) will see proof rejections until the resource URI is corrected to match the canonical origin. core/resource/verifier:(*VerifiedClaims).RequireScopedelegates toRequireScopes, so the singular helper also emits the enrichedrequired scope "X"; token has scopes: …error_description. Behaviour (errors.Is(err, ErrInsufficientScope), 403 status,scope="…"parameter) is unchanged; only the error string is enriched.- BREAKING
core/resource/verifier:DPoPContextno longer exposes the raw proof through a public field — the previousDPoPContext.Proof stringis replaced with an unexported slice plus the(*DPoPContext).Proof()accessor and theNewDPoPContextconstructor. Route through the factory so RFC 9449 §4.3 #1 enforcement stays single-source and invalid (multi-proof) states stay unconstructable. http: HTTP adapter middleware readsr.Header.Values("DPoP")and routes the slice throughNewDPoPContext. A request carrying more than oneDPoPheader returns HTTP 401 +WWW-Authenticate: DPoP error="invalid_dpop_proof"(RFC 9449 §4.3 #1, §7.1); the previousr.Header.Get("DPoP")silently picked only the first copy.http:WWW-Authenticatenow uses a space (not a comma) beforeresource_metadatawhen no prior auth-param is present, matching RFC 7235 §2.1 (Bearer resource_metadata="…"instead ofBearer, resource_metadata="…").mcp.NewAdapterFromClientAndResourcenow returns(*Adapter, error)and rejects a nil client with a typed error instead of panicking, matching the shape of its discovery-drivenNewAdaptersibling.