Skip to content

Releases: AuthPlane/go-sdk

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 01 Oct 16:17

Released commit: 2371d7a143dbd9f44185d08152c1ddb4fe334ab8

Added

  • core/resource: WithResourceMetadataURL(string) option and Resource.ResourceMetadataURL() accessor point the resource_metadata challenge parameter at an AS-hosted RFC 9728 document; the http, mcp and mark3labs adapters advertise it via Options.ResourceMetadataURL.
  • core/resource: AuthErrorResponseWithMetadata(err, resourceMetadataURL, realm...) emits the RFC 9728 §5.1 resource_metadata parameter that the http adapter used to append itself; the header is byte-identical.
  • core/authplane: ErrAccessDenied and ErrInvalidTarget sentinels for the token-endpoint errors access_denied (403, cross-client exchange not allowlisted on the target Resource) and invalid_target (400, RFC 8707 §2.2); match with errors.Is.
  • core/authplane: the auto-wired introspection checker warns once per resource when the AS answers active: false for a locally valid token, pointing at the runtime-client requirement (authserver ≥ 0.1.2).
  • core/resource/verifier: the fail-open revocation branch logs a log/slog warning carrying the checker error instead of accepting the token silently.
  • http, mcp, mark3labs: every 401 WWW-Authenticate challenge now carries scope="…" 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) — AuthErrorResponse with the error's own message restored in the JSON error_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 unknown kid no longer costs one JWKS fetch per verification; forced refreshes have a retry floor of min(jwksCacheTTL, 1m). Impact: a newly rotated kid can 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 says invalid_request; like the challenge, it now omits error.

Changed

  • core/authplane: access_denied and invalid_target no longer count toward the circuit breaker — both are policy answers about the request, not an AS outage.
  • BREAKING core/resource: AuthErrorResponse emits a fixed error_description per error code instead of the error's message; RequireScopes no longer lists missing scopes in the body. Migration: the net/http adapter logs the diagnostic at DEBUG; log err yourself elsewhere, or use AuthErrorResponseVerbose in development.
  • BREAKING core/resource: resource.New rejects 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 issues may_act; removed in the next minor. Parsing is unchanged until then.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 20:01

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.New and authplane.NewClient all route through it.
  • core/resource/verifier: ErrInvalidIssuer sentinel, returned by everything that validates an issuer identifier. Match it with errors.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=…), and net/url.Error prints 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, so errors.As(err, new(*url.Error)) keeps working; only the URL the error prints is substituted.
  • http: the RFC 9728 PRM discovery bypass in the net/http adapter now compares r.URL.EscapedPath() against the escaped well-known path instead of the decoded r.URL.Path. A resource identifier carrying a percent-encoded octet (e.g. %2F) yields an escaped well-known path; comparing the decoded path let %2F collapse 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: NewTokenVerifier and resource.New now 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.ParseRequestURI alone accepted both: it takes a path-only reference, and it folds a fragment into Path rather than splitting it. Migration: pass the authorization server's issuer identifier exactly as published — absolute, https, no query, no fragment.
  • BREAKING core/authplane: NewClient additionally 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 *Client used only for token, introspection and revocation calls never constructs a TokenVerifier, so it is the only thing keeping a relative reference out of eager discovery. Migration: as above.
  • BREAKING core/authplane: ErrInvalidIssuer is now an alias of verifier.ErrInvalidIssuer rather than its own sentinel. Two consequences for code that inspects it: the message changes from authplane: invalid issuer to verifier: invalid issuer, and errors.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 did errors.Is(err, authplane.ErrInvalidIssuer) on a NewClient error is unaffected.
  • BREAKING core/authplane: NewClient now 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 to NewClient; an issuer identifier never carries one.
  • BREAKING core/resource: resource.New now rejects a resource URI containing a # (RFC 8707 §2 forbids a fragment in a resource indicator). url.ParseRequestURI does not split the fragment, so https://api.example.com/mcp#frag previously 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 to resource.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/mcp rather 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() and PRMURL() 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 as https://api.example.com/mcp%2Fx therefore yields .../oauth-protected-resource/mcp%2Fx where 0.2.0 returned .../mcp/x — a visible output change on both exported methods. Migration: if you consume these values (routing the PRM handler, advertising resource_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's issuer byte-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 at NewClient as metadata: 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 to NewTokenVerifier verbatim and matches a token's iss claim byte-for-byte (RFC 8414 §4: code-point-for-code-point, no normalization) instead of trailing-slash-insensitively. A token whose iss differs from the configured issuer only by a trailing slash is now an ErrIssuerMismatch. Migration: If the issuer you pass to NewTokenVerifier differs from your authorization server's actual identifier by a trailing slash, correct it — the SDK no longer silently reconciles them.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 15:28

Released commit: 6c81739453cb1d3937ef52d0de2d5c4c8a50d894

Added

  • core/resource/verifier: (*VerifiedClaims).RequireScopes(scopes ...string) error plural helper. Returns nil on empty input, and on failure wraps ErrInsufficientScope naming every missing scope plus the scopes the token does carry, so an adapter can surface it verbatim in the WWW-Authenticate error_description.
  • core/resource/verifier: ErrMultipleDpopProofs sentinel for RFC 9449 §4.3 #1 violations. core/resource.AuthErrorResponse maps it to a DPoP 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 returns ErrMultipleDpopProofs on more than one non-blank value.
  • core/resource/verifier: (*DPoPContext).Proof() nil-safe accessor returning the single proof (or "" when none).
  • mark3labs module: adapter for mark3labs/mcp-go. Wraps *authplanehttp.Adapter so consumers get bearer + DPoP auth, RFC 9728 PRM, and HTTPContextFunc integration for the mark3labs HTTP server. HTTPContextFunc takes WithForwardedContextKeys(keys ...any) and WithContextForwarding(fn) options to propagate values (request IDs, tracing spans, …) from the upstream request context onto the per-tool-call MCP context. See mark3labs/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 typed PRMConfig struct, for feeding into third-party PRM-serving handlers (e.g. mark3labs/mcp-go's NewProtectedResourceMetadataHandler) without going through the dynamic PRMResponse map.
  • core/authplane: TokenResponse/IntrospectionResponse expose the RFC 9449 §6 confirmation — Cnf json.RawMessage (raw cnf, verbatim) and CnfJkt string (DPoP thumbprint from cnf.jkt, empty when unbound). Derived at parse time and preserved across TokenCache hits.

Fixed

  • core/resource/verifier: validateHTU compares EscapedPath() instead of Path, so an encoded %2F is no longer treated as equivalent to a literal /. The previous comparison conflated distinct request targets, weakening the RFC 9449 §4.3 htu binding (RFC 3986 §6.2.2.2 only permits decoding unreserved characters when comparing URLs).
  • core/resource/verifier: validateHTU strips an explicit default port (:80 for http, :443 for https) on both sides before comparing (RFC 9110 §7.2), so a resource configured as http://api.example.com:80/mcp no longer mismatches every client that signs the port-less form.
  • core/resource/verifier: validateHTU collapses 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 inbound r.URL.EscapedPath() is at least /.
  • core/internal/cache: TokenCache.Set distinguishes a missing expires_in from expires_in: 0 (RFC 6749 §5.1) — nil applies the default TTL, 0 is refused as born-expired instead of cached for the default hour.
  • core/internal/dpop: NormalizePath upper-cases percent-encoded hex triplets (RFC 3986 §6.2.2.1) on both comparison sides, so a proof signed with %2f matches a request reconstructed with %2F.

Changed

  • BREAKING (pre-1.0) core/authplane: TokenResponse.ExpiresIn changes from int64 to *int64, so a response that omits expires_in is now nil rather than 0. This distinguishes an absent field from an explicit expires_in: 0 (RFC 6749 §5.1 permits a deliberately-expired one-shot token) and aligns the Go shape with the optional expires_in wire contract (absent vs. 0 vs. positive). Migration: any caller reading resp.ExpiresIn directly (arithmetic, comparison, formatting) must dereference and nil-check; treat nil as "apply your default" and *v == 0 as "already expired".
  • BREAKING (pre-1.0) http: DPoP htu reconstruction in the net/http adapter no longer reads the inbound Host header, r.TLS, or r.URL.RawQuery. Both Host and r.TLS are proxy-controlled and would otherwise let a misconfigured edge — or an attacker forging Host — shift the htu binding to a different origin or downgrade https to http behind a TLS-terminating proxy. The adapter now sources scheme + authority from the operator-configured resource URI and contributes only the request path in raw EscapedPath form (query and fragment are dropped per RFC 9449 §4.3 #5). Migration: mount the middleware before any http.StripPrefix so r.URL.EscapedPath() still reflects the path the client signed; apps relying on Host-derived htu (or r.TLS-derived scheme) will see proof rejections until the resource URI is corrected to match the canonical origin.
  • core/resource/verifier: (*VerifiedClaims).RequireScope delegates to RequireScopes, so the singular helper also emits the enriched required 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: DPoPContext no longer exposes the raw proof through a public field — the previous DPoPContext.Proof string is replaced with an unexported slice plus the (*DPoPContext).Proof() accessor and the NewDPoPContext constructor. 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 reads r.Header.Values("DPoP") and routes the slice through NewDPoPContext. A request carrying more than one DPoP header returns HTTP 401 + WWW-Authenticate: DPoP error="invalid_dpop_proof" (RFC 9449 §4.3 #1, §7.1); the previous r.Header.Get("DPoP") silently picked only the first copy.
  • http: WWW-Authenticate now uses a space (not a comma) before resource_metadata when no prior auth-param is present, matching RFC 7235 §2.1 (Bearer resource_metadata="…" instead of Bearer, resource_metadata="…").
  • mcp.NewAdapterFromClientAndResource now returns (*Adapter, error) and rejects a nil client with a typed error instead of panicking, matching the shape of its discovery-driven NewAdapter sibling.

v0.1.1

Choose a tag to compare

@github-actions github-actions released this 22 May 17:17

Released commit: 137eb5830a069bf45fb3a02c8eae95c3b2355504

Fixed

  • DPoP htu validation against Authorization Servers on non-default ports. The
    SSRF-safe pinned HTTP client now keeps a non-default port in the Host header,
    and DPoP proof generation normalizes the htu claim to match: an explicit
    default port (:80/:443) is dropped, non-default ports are preserved, IPv6
    literals stay bracketed, and any userinfo is stripped from htu
    (RFC 9110 §7.2, RFC 9449 §4.3).

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 17 May 14:08

Released commit: 4f34f790697a800c27a5cc7e79350332822334b3

  • Initial release.