Repository navigation
Releases: AuthPlane/cs-sdk
Release list
v0.2.0
Released commit: 8bb3826c6adce78490f125676fe61fb3f84eb84c
Added
AccessDeniedException(access_denied, 403) andInvalidTargetException(invalid_target, 400) typed byMapOAuthError; neither counts toward the circuit breaker.AuthplaneMcpAuth.Options.ResourceMetadataUrlpoints theresource_metadatachallenge at an AS-hosted PRM document; validated at construction with the resource identifier's rules, andhttp://authserver:8080/...is accepted.- README Compatibility section: tested against authserver 0.2.0; introspection-based revocation requires authserver 0.1.2 or later.
- Resource-server DPoP nonce enforcement (RFC 9449 §9), opt-in via
InboundDPoPOptions.nonceIssuer;null, the default, leaves existing deployments unchanged. When enabled, every client's first DPoP request takes a 401use_dpop_nonceround trip. - Inbound DPoP nonce API:
IDPoPNonceIssuer,HmacDPoPNonceIssuer(key required;CreateEphemeral()for single-process),DPoPNonceRequiredException,AuthplaneErrors.ResponseHeadersandVerifiedClaims.NextDPoPNonce. Custom adapters must copy both onto the response. AuthplaneErrors.ErrorResponseBody(...),ErrorDescriptionFor(code)andErrorCodeFor(error)build the RFC 6750 §3 JSON error body and fixed description from the challenge's error code.Authplane.Mcp— the middleware logs the exception behind every failure:Errorfor a 5xx,Debugfor a rejection. Hosts without anILoggerFactoryget no output.
Changed
TokenRevokedExceptionfrom anactive=falseintrospection now names the other cause: the AS not recognising this resource server as the token's owner (authserver ≥ 0.1.2 runtime-client rule).- User guides document
access_deniedvsconsent_requiredon token exchange, theallowed_client_idsoperator step, and the confidential + runtime-client requirement for introspection. manual-e2e-setup.shno longer setsAUTHPLANE_CLIENT_CREDENTIALS_ENABLED(on by default since authserver 0.2.0) and acceptsAUTHSERVER_REFto check out an authserver ref before building.manual-e2e-smoke.shno longer callsPOST /admin/scopes(the route does not exist in authserver 0.2.0; the demo provisioner creates the scopes).OAuthProtectedResourceMetadata.GetDocumentUrlderives the document URL from the configured identifier string verbatim, query included. Migration: an identifier with an uppercase scheme or host, a default port or dot-segments now advertises a different URL; update hard-coded expectations.- Breaking A resource identifier must be an absolute URL with a scheme and host, with no fragment, userinfo, whitespace, backslash, C0 control or DEL, a port of in-range digits with no leading zero, and an RFC 3986 path and query; anything else throws
ArgumentExceptionat construction. Migration: configure the full URL and percent-encode offending octets. - The same identifier gates run in
ProtectedResourceMetadata's constructor andBuild. - BREAKING
AuthplaneErrors.WwwAuthenticate(...)now emits a fixederror_descriptionchosen by theerror=code instead of the exception message. Migration: logerror.Messageserver-side, or passverboseDescription: true. - BREAKING
Authplane.Mcp— every failure answers with an RFC 6750 §3 JSON body and a fixed description pererrorcode, instead of prose and the exception message. Migration: parseerroranderror_descriptionfrom the body. - BREAKING
Authplane.Mcp— a 503 (JWKS or metadata unreachable) answerstemporarily_unavailableinstead ofserver_error. Migration: matchtemporarily_unavailableto detect a transient outage.
Deprecated
VerifiedClaims.MayActmarked[Obsolete]: authserver 0.2.0 no longer issuesmay_act; removed in the next minor.
Fixed
- The MCP middleware's decoded-path fallback held
%5Cback from decoding while Kestrel decodes it, so a%5C-bearing resource identifier answered 401 at its own advertised metadata URL on hosts without a raw request target. AuthplaneResource.CreateAsyncno longer abandons theAuthplaneClientit builds when the resource constructor rejects its arguments;DisposeAsyncis now idempotent.- The authority now has an RFC 3986 §3.2.2 character-production gate, so an internationalized host is rejected at construction instead of reaching a
WWW-Authenticatechallenge as a non-URI. - An authority that is only userinfo (
https://user@) is reported as a missing host withArgumentException, instead of throwingIndexOutOfRangeException. AuthplaneClient.CreateAsyncno longer abandons the client it built when the priming metadata fetch fails or the caller's token is cancelled.- An opaque resource identifier such as
mailto:ops@example.comis no longer reported as carrying userinfo; it is still refused, now because an opaque URI has no host to derive a metadata document URL from. - The MCP middleware answers a JWKS or metadata outage with 503 instead of 401
invalid_token, and sendsWWW-Authenticateonly on a 401.
v0.1.0
Released commit: 8def72519b264c01f9dcbe6d661e8f483fdcdb39
Added
-
Explicit RFC 9449 §4.3 #1 enforcement: the new
DPoPRequestContext.FromHeaderValuesfactory rejects requests carrying
more than oneDPoPproof — as repeated header entries or as a single
comma-folded value produced by a header-combining intermediary
(RFC 9110 §5.3) — with the newDPoPMultipleProofsException, surfaced
as aDPoP-scheme challenge witherror="invalid_dpop_proof"
(RFC 9449 §7.1) by bothAuthplaneErrors.WwwAuthenticateand the MCP
middleware. Only this rejection carries that code; the other DPoP
failures keepinvalid_token. -
Both packages now multi-target
net8.0;net10.0and embed the Authplane
package icon. -
Authplane.Conformance.Sharedtest library with[Conformance]attribute,
ConformanceTracker, andConformanceCatalogAlignmentguard so
conformance assertions are tagged and tracked against the shared catalog. -
AuthplaneAuthClient.RevokeAsync(RFC 7009 token revocation). -
IDPoPNonceStore/InMemoryDPoPNonceStorefor outbound DPoP nonce handling. -
JwksCachewith background refresh at 80% TTL, stale-cache fallback,
force-refresh onkidmiss, and lock-coordinated fetches. -
Proper SSRF hardening (
Net/IpValidation.cs,Net/Ssrf.cs) with DNS pinning,
anti-rebinding TOCTOU, cloud-metadata IP block, response-size limits, and
no-redirects. -
JwksFetchSettingsandMetadataFetchSettingsfor asymmetric outbound
fetch policy. -
IRevocationChecker+IntrospectionRevocation+failClosedflag on
AuthplaneResource. -
CONTRIBUTING.md,SECURITY.md,CHANGELOG.md(this file). -
Root README Capabilities section listing every implemented RFC, security
feature, framework integration, and observability hook. -
Per-package
docs/user-guide.mdforAuthplane.SdkandAuthplane.Mcp. -
.editorconfig,Directory.Build.props,global.json,.pinact.yaml. -
CI:
dotnet format --verify-no-changesstep, conformance catalog clone,
upload ofconformance-report.{json,md}as workflow artifact. -
Packaging: SourceLink +
.snupkgsymbol packages for downstream
debugger step-through. Wired implicitly viaPublishRepositoryUrl=true
on the bundled .NET 10 SDK SourceLink — no explicitMicrosoft.SourceLink.GitHub
package reference required.IncludeSymbols=true+
SymbolPackageFormat=snupkgproduces a.snupkgnext to each.nupkg;
the publish workflow pushes both to nuget.org.
Changed
OAuthProtectedResourceMetadata.GetDocumentUrlnow removes the trailing
slash following the host component before inserting the well-known path
suffix, per RFC 9728 §3.1. A resource configured as
https://api.example.com/mcp/previously derived (and the MCP middleware
served/advertised)/.well-known/oauth-protected-resource/mcp/; it now
derives/.well-known/oauth-protected-resource/mcp. Only the document-URL
derivation changed — the resource identifier itself is still stored,
advertised, and compared exact-string everywhere else. A percent-encoded
%2Fin the final path segment is data, not a delimiter (RFC 3986 §3.3),
and survives the trim.src/Authplane/reorganised intoOAuth/,Verifier/,Net/,DPoP/,
Resilience/, andMetadata/subfolders. The public namespace remains
Authplane; no API breaking changes.OAuthOperations.cs(460 LOC) split into focused internals under
OAuth/Internal/(OAuthHttpClient,OAuthRequestBodies,
OAuthResponseParser,OAuthErrorResponse).- All OAuth client exceptions (
AuthplaneTokenRequestException,
ConsentRequiredException, parsing exceptions,ServerError) consolidated
inErrors.cs. AuthplaneVerifier.csrenamed toAuthplaneResource.cs(matches the class
it contains).- Root
README.mdrewritten as a user-facing intro per
sdk-documentation-conventions.md. Build/test/coverage commands moved to
CONTRIBUTING.md. - Per-package READMEs rewritten as short hero pages with one quickstart and a
link to the user guide;dotnet restore/build/testcontent moved to
CONTRIBUTING.md. - Coverage thresholds raised from 60/45 (line/branch) to 80/70.
- CI now runs both
Authplane.TestsandAuthplane.Mcp.Tests; the
--filter "FullyQualifiedName!~Conformance"exclusion is removed. - Smoke scripts moved from
demo/toscripts/. manual-e2e-smoke.shregisters required scopes against the authserver
before minting a token.demo/Authplane.Mcp.Demo.csprojnow declaresIsPackable=false, matching
the hygiene flag the test and conformance projects already carry. The
demo isOutputType=Exeso it was never producing a.nupkg; this just
makes the intent explicit.- Breaking (verifier). Inbound DPoP
htmcomparison is now byte-exact
ordinal per RFC 9449 §4.3 step 11 / RFC 9110 §9.1 method-token semantics.
A proof whosehtmdiffers from the request method only in case (e.g.
htm:"post"for aPOSTrequest) is now rejected with
InvalidDPoPProofException. The previous behaviour case-folded both
sides and silently accepted such proofs.
Clients that emit lowercasedhtmmust be updated. AuthplaneMcpAuth.Optionsaccepts anInboundDPoPOptions? inboundDpop
parameter and propagates it to the underlyingAuthplaneResource. The
MCP middleware's pre-tokenWWW-Authenticatechallenge scheme now
follows the configured DPoP mode:Bearer-only when DPoP is off,
DPoP-only whenRequired=true, combinedBearer+DPoPotherwise.
Previously the challenge always advertised both schemes regardless of
whether the resource accepted DPoP.ES256DpoPSignernow implementsIDisposableand releases the
underlyingECDsaprivate key. Long-lived processes that rotate signers
no longer leak native handles.AuthplaneAuthClient.DisposeAsyncalso clears the in-memory
TokenCacheso disposal releases the access tokens it was holding.- Default
tokenTypeHintparameters now reference
OAuthConstants.TokenTypeHintAccessTokeninstead of the bare string
literal; the values are unchanged.
Deprecated
AuthplaneVerifierlegacy wrapper marked[Obsolete]; will be removed in
v0.2.0. Migrate toAuthplaneResource.
Removed
- The cosmetic
ConformanceTests.csrunner (() => Task.CompletedTaskper
case) and the misleading100 / 100 passedreport it produced. Replaced
with a realConformanceCatalogAlignmentTestsguard. - ~516 LOC of duplicated conformance plumbing across
Authplane.Testsand
Authplane.Mcp.Tests.
Fixed
- Broken references to
Authplane.sln(the file isAuthplane.slnx). demo/README.mdclaimed.NET 8 SDKwhileAuthplane.csprojtargeted
net10.0.JwksCache/MetadataCachebackground refresh used to get permanently
stuck after a single call from a cancelledCancellationToken. The
outerTask.Runreceived the caller's CT; when the CT was already
cancelled at schedule time the task enteredCanceledstate and the
finally{}that clears_backgroundRefreshnever ran. From that
point on_backgroundRefreshstayed pinned to a never-completing
Taskand no further background refresh was ever triggered for the
lifetime of the cache — degrading silently to the 24h_maxStaleAge
fallback. Task now schedules withCancellationToken.None.AuthplaneClient.FetchMetadatano longer swallows transport errors
with a barecatch { }. When every discovery URL fails for
transport reasons, the last transport exception is now attached as
theInnerExceptiononMissingMetadataEndpointException.IntrospectionRevocation.IsRevokedAsyncno longer swallows
CircuitOpenExceptionwhen configuredfailOpen: true. A tripped
circuit (AS observably unhealthy) now propagates so the
resource-levelfailClosed/failOpenpolicy can decide.
Previously the lenient I/O-error handling silently accepted any
possibly-revoked token during an AS outage.- Caller cancellation (
OperationCanceledException) now propagates
through the revocation check path instead of being translated to
fail-open accepted. AuthplaneResource.DecodeHeadernow uses
Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes
instead of a hand-rolled+/-substitution + padding helper. No
behavioural change.WWW-Authenticatechallenges emitted fromAuthplaneMcpAuthno
longer over-advertise DPoP when the configuredAuthplaneResource
rejects DPoP-bound tokens. Previously a client that picked DPoP
from the challenge negotiated acnf.jkt-bound token and saw every
request rejected asDPoPNotSupportedException.
Added
Authplane.OAuthEndpoints(internal) — single source of truth for the
/oauth/token,/oauth/introspect,/oauth/revokeendpoint paths.Authplane.OAuthRequestBodies.BuildTokenForm(token, hint?)for the
shared introspection / revocation parameter shape.Authplane.JsonHelpers(internal) —GetStringOrNull,
GetInt64OrNull,GetBoolOrNull,GetStringArrayOrEmptyextension
methods onJsonElement, replacing inline
TryGetProperty + ValueKind + Get*()boilerplate.Authplane.Base64Url,Authplane.DPoPHashes,
Authplane.JwkThumbprint,Authplane.DPoPProofBuilder,
Authplane.DPoPDefaults(all internal) — single source of truth for
base64url encoding, the DPoPathdigest, the RFC 7638 JWK
thumbprint, the proof JWT shape, and the proof TTL / clock-skew
defaults. Previously 3–4 near-identical copies of each lived across
DPoPKeyMaterial,DPoPProvider,ES256DpoPSigner, and
AuthplaneResource.MissingMetadataEndpointExceptionoverload accepting an
InnerExceptionfor transport-failure causes.OAuthConstantsexpanded with nested static classes covering OAuth
form-body parameters, RFC 6750 / 9449 error codes, HTTP header
names, MIME types, auth scheme prefixes, RFC 8414 / 9728 well-known
paths, JOSE algorithm identifiers, JWT claim na...