Releases: AuthPlane/go-sdk
Releases · AuthPlane/go-sdk
Release list
v0.4.0
Released commit: 2371d7a143dbd9f44185d08152c1ddb4fe334ab8
Added
core/resource:WithResourceMetadataURL(string)option andResource.ResourceMetadataURL()accessor point theresource_metadatachallenge parameter at an AS-hosted RFC 9728 document; thehttp,mcpandmark3labsadapters advertise it viaOptions.ResourceMetadataURL.core/resource:AuthErrorResponseWithMetadata(err, resourceMetadataURL, realm...)emits the RFC 9728 §5.1resource_metadataparameter that thehttpadapter used to append itself; the header is byte-identical.core/authplane:ErrAccessDeniedandErrInvalidTargetsentinels for the token-endpoint errorsaccess_denied(403, cross-client exchange not allowlisted on the target Resource) andinvalid_target(400, RFC 8707 §2.2); match witherrors.Is.core/authplane: the auto-wired introspection checker warns once per resource when the AS answersactive: falsefor a locally valid token, pointing at the runtime-client requirement (authserver ≥ 0.1.2).core/resource/verifier: the fail-open revocation branch logs alog/slogwarning carrying the checker error instead of accepting the token silently.http,mcp,mark3labs: every 401WWW-Authenticatechallenge now carriesscope="…"listing the resource's configured scopes (RFC 6750 §3; MCP authorization spec SHOULD), omitted when none are configured. 403 challenges are unchanged.core/resource:AuthErrorResponseVerbose(err error, realm ...string)—AuthErrorResponsewith the error's own message restored in the JSONerror_description. A development aid: it discloses SDK-internal detail to unauthenticated callers, so do not use it in production.
Fixed
core/resource: a resource URI rejected at construction no longer appears unredacted in the error when it carries credentials;errors.As(err, new(*url.Error))keeps working.core/internal/cache: an unknownkidno longer costs one JWKS fetch per verification; forced refreshes have a retry floor ofmin(jwksCacheTTL, 1m). Impact: a newly rotatedkidcan be rejected until that floor elapses.core/internal/cache: a server expiry at or before caching time no longer leaves the document permanently expired, and a server expiry longer than the configured interval is clamped to it.core/resource: the JSON error body for a request with no credentials no longer saysinvalid_request; like the challenge, it now omitserror.
Changed
core/authplane:access_deniedandinvalid_targetno longer count toward the circuit breaker — both are policy answers about the request, not an AS outage.- BREAKING
core/resource:AuthErrorResponseemits a fixederror_descriptionper error code instead of the error's message;RequireScopesno longer lists missing scopes in the body. Migration: thenet/httpadapter logs the diagnostic at DEBUG; logerryourself elsewhere, or useAuthErrorResponseVerbosein development. - BREAKING
core/resource:resource.Newrejects a resource URI with a literal space in the path, a"in the host, userinfo, or a query that is not valid RFC 3986. Migration: percent-encode the offending octets and remove credentials. - BREAKING
core/resource:PRMURL()now keeps the resource identifier's query in the derived PRM URL (RFC 9728 §3);WellKnownPRMPath()is unchanged. Migration: update any hard-coded expectation of the query-less URL.
Deprecated
core/resource/verifier:(*VerifiedClaims).MayAct()— authserver 0.2.0 no longer issuesmay_act; removed in the next minor. Parsing is unchanged until then.
v0.3.0
Released commit: 841793dbe22bfeb36456ee760f9fb2a201d64ab9
Added
core/resource/verifier:ValidateIssuer(issuer string) error— the RFC 8414 §2 issuer-shape rule, exported so every construction boundary applies one implementation rather than a copy. Rejects a query or fragment component, and requires an absolute URL with a scheme and host.NewTokenVerifier,resource.Newandauthplane.NewClientall route through it.core/resource/verifier:ErrInvalidIssuersentinel, returned by everything that validates an issuer identifier. Match it witherrors.Is.
Fixed
core/resource/verifier,core/authplane: an issuer rejected at construction is no longer echoed verbatim into the error. The query/fragment branch fires for exactly the shape that can carry a credential (https://as.example.com?access_token=…), andnet/url.Errorprints its URL field without redacting, so the raw identifier — query, fragment and any userinfo — reached whatever log the construction error landed in. Messages now carry scheme and host only. Parse failures are still wrapped with%w, soerrors.As(err, new(*url.Error))keeps working; only the URL the error prints is substituted.http: the RFC 9728 PRM discovery bypass in thenet/httpadapter now comparesr.URL.EscapedPath()against the escaped well-known path instead of the decodedr.URL.Path. A resource identifier carrying a percent-encoded octet (e.g.%2F) yields an escaped well-known path; comparing the decoded path let%2Fcollapse to/, the two sides disagreed, and the discovery endpoint stopped being bypassed and returned 401 even though RFC 9728 §3.2 requires it publicly reachable. The check is deliberately stricter than RFC 3986 §6.2.2.1 (a percent-encoded unreserved octet won't match its decoded form), an accepted trade-off since a conformant client signs the same octets the operator configured.
Changed
- BREAKING
core/resource/verifier,core/resource:NewTokenVerifierandresource.Newnow reject an issuer carrying a query or fragment component, and require the identifier to be an absolute URL with a scheme and host (RFC 8414 §2). Construction that succeeded in 0.2.0 — a relative reference such as/tenant, or an issuer with?x=1— now fails.url.ParseRequestURIalone accepted both: it takes a path-only reference, and it folds a fragment intoPathrather than splitting it. Migration: pass the authorization server's issuer identifier exactly as published — absolute,https, no query, no fragment. - BREAKING
core/authplane:NewClientadditionally requires the issuer to be absolute with a scheme and host, beyond the query/fragment rule below. This gate is not redundant with the verifier's: a*Clientused only for token, introspection and revocation calls never constructs aTokenVerifier, so it is the only thing keeping a relative reference out of eager discovery. Migration: as above. - BREAKING
core/authplane:ErrInvalidIssueris now an alias ofverifier.ErrInvalidIssuerrather than its own sentinel. Two consequences for code that inspects it: the message changes fromauthplane: invalid issuertoverifier: invalid issuer, anderrors.Is(err, authplane.ErrInvalidIssuer)now returns true for a rejection raised by the verifier, where it previously returned false. Migration: if you relied on the two sentinels being distinct to tell which layer rejected an identifier, that distinction is gone — both boundaries now apply the same rule, so match on the single sentinel and read the message for the specific violation. Code that only diderrors.Is(err, authplane.ErrInvalidIssuer)on aNewClienterror is unaffected. - BREAKING
core/authplane:NewClientnow rejects an issuer containing a query or fragment component (RFC 8414 §2 forbids both) instead of passing it straight into metadata discovery. Previously the resource side rejected a fragment but the issuer had no such check, and the two discovery-URL builders diverged when either was present — the RFC 8414 builder silently dropped the issuer's query/fragment while the OIDC builder carried them along, so the two discovery attempts targeted different identities. Construction now fails immediately with a clear error. Migration: strip any query or fragment from the issuer you pass toNewClient; an issuer identifier never carries one. - BREAKING
core/resource:resource.Newnow rejects a resource URI containing a#(RFC 8707 §2 forbids a fragment in a resource indicator).url.ParseRequestURIdoes not split the fragment, sohttps://api.example.com/mcp#fragpreviously passed the scheme/host check and leaked the fragment into the derived PRM URL. This is a construction-time change on the exported constructor. Migration: remove any fragment from the resource URI you pass toresource.New. - BREAKING
core/resource: the RFC 9728 §3.1 PRM well-known URL now strips any terminating slash following the host component before inserting the well-known path suffix, so a resource identifier ending in/mcp/is served at (and derived by a conformant client as)/.well-known/oauth-protected-resource/mcprather than.../mcp/. The resource identifier itself is unchanged — only the derived publication URL loses the slash. Migration: if you currently serve your PRM document at a trailing-slash well-known path, move it to the slash-stripped path (or route both) so RFC 9728 clients stop 404ing. - BREAKING
core/resource:WellKnownPRMPath()andPRMURL()now derive from the resource identifier's escaped path, so a percent-encoded octet (RFC 3986 §3.3 path data, e.g.%2F) is carried through verbatim instead of being decoded to/. A resource identifier such ashttps://api.example.com/mcp%2Fxtherefore yields.../oauth-protected-resource/mcp%2Fxwhere 0.2.0 returned.../mcp/x— a visible output change on both exported methods. Migration: if you consume these values (routing the PRM handler, advertisingresource_metadata), ensure your router matches the escaped path. - BREAKING
core/internal/metadata: the RFC 8414 §3.3 issuer check now compares the configured issuer and the metadata document'sissuerbyte-for-byte (§4: code-point-for-code-point, no normalization) instead of trailing-slash-insensitively. A document whose issuer differs from the configured issuer only by a trailing slash is now rejected as a mismatch. Because discovery is eager, this surfaces atNewClientasmetadata: issuer mismatch— construction fails immediately, not at the first token verification. Migration: If your configured issuer differs from your authorization server's actual identifier by a trailing slash, correct the config — the SDK no longer silently reconciles them. - BREAKING
core/resource/verifier: the token verifier stores the issuer passed toNewTokenVerifierverbatim and matches a token'sissclaim byte-for-byte (RFC 8414 §4: code-point-for-code-point, no normalization) instead of trailing-slash-insensitively. A token whoseissdiffers from the configured issuer only by a trailing slash is now anErrIssuerMismatch. Migration: If the issuer you pass toNewTokenVerifierdiffers from your authorization server's actual identifier by a trailing slash, correct it — the SDK no longer silently reconciles them.
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.
v0.1.1
Released commit: 137eb5830a069bf45fb3a02c8eae95c3b2355504
Fixed
- DPoP
htuvalidation against Authorization Servers on non-default ports. The
SSRF-safe pinned HTTP client now keeps a non-default port in theHostheader,
and DPoP proof generation normalizes thehtuclaim to match: an explicit
default port (:80/:443) is dropped, non-default ports are preserved, IPv6
literals stay bracketed, and any userinfo is stripped fromhtu
(RFC 9110 §7.2, RFC 9449 §4.3).
v0.1.0
Released commit: 4f34f790697a800c27a5cc7e79350332822334b3
- Initial release.