Releases: cirreum/Cirreum.AuthenticationProvider
Release list
Release v2.0.3
Full Changelog: v2.0.2...v2.0.3
Release v2.0.2
Release v2.0.1
Release v2.0.0
Cirreum.AuthenticationProvider 2.0.0 — Revocations Can Now Expire
Why this release exists
IRevokedCredentialProvider could only say which credentials were revoked. That left the two
revocation paths behaving differently for a reason no consumer could see.
A revocation arriving as a live CredentialRevoked event carries the credential's expiry, so the
in-memory denylist drops the entry once the credential could no longer authenticate anyway —
IApiKeyDenylist.Revoke(id, expiresAt) has always accepted one. A revocation hydrated at boot
had no expiry to pass, so it was retained until the process restarted.
The plumbing existed at both ends. The contract in the middle was the only thing that couldn't carry
expiry.
public readonly record struct RevokedCredential(
string CredentialId,
DateTimeOffset? ExpiresAt = null);
IAsyncEnumerable<RevokedCredential> GetRevokedCredentialsAsync(CancellationToken ct = default);Supplying the expiry is optional. Omit it and behavior is exactly as before — the entry is
retained until restart, which is safe: over-retention costs memory, under-revocation would re-admit a
credential. Supply it and the entry self-evicts, which is what makes a large or long-lived revoked
population affordable to hold in memory.
It is a readonly record struct because this streams through IAsyncEnumerable<T> on the boot path,
and the populations that make expiry worth carrying are exactly the large ones — where an allocation
per revocation is the cost worth not paying.
The member is renamed rather than overloaded, so an implementer gets a clean compile error instead of
a type mismatch on a method whose name still says "Ids".
Also removed
AuthenticationDiagnostics is removed. That is the whole of the breaking change, and almost
certainly nothing to do — the class had no references anywhere, in this framework or in the one
application integrating against it.
It is removed rather than deprecated because it was worse than dead. Its single member published a
telemetry name:
public const string DiagnosticName = "Cirreum.AuthenticationProvider";That name is not among the sources and meters Kernel's AddCirreum() subscribes. Telemetry names in
Cirreum are a cross-package contract, and a source whose name is never registered is silently
inert — it records into the void, with no listener attached and nothing failing to indicate it. The
class documentation invited exactly that use, so anyone following it would have shipped telemetry
that never reached an exporter. Same defect that left identity-provisioning telemetry unobservable
until Cirreum.Kernel 1.3.0 registered it.
Runtime authentication telemetry has always used Cirreum.Authentication, which is registered. If
you referenced the removed constant, use CirreumTelemetry.ActivitySources.Authentication or
CirreumTelemetry.Meters.Authentication — and check whether you need it at all, since AddCirreum()
already subscribes those names.
Worth reading even though it isn't breaking
IRevokedCredentialProvider now documents an operational constraint that was previously only
implicit: keep your persisted revoked set bounded.
Everything the provider yields is held in memory for the process lifetime, and the in-memory denylist
is capacity-bounded — on saturation it fails authentication closed rather than silently dropping
a revocation. An unbounded revoked set therefore degrades into refused authentication, not into
stale state. That is the correct trade, and it is not the failure mode most people would guess.
The safe pruning rule is the denylist's own: remove a revocation once the credential could not
authenticate anyway — past its expiry plus any grace window, or once deleted or rotated out of
issuance. Never prune a live, non-expired credential's revocation — that re-admits it.
No behavior changed here. The rule was always true; it simply wasn't written down.
Compatibility
Breaking only through the removal above, which is a compile error rather than a silent change.
See MIGRATION-v2.md.
Coordinated downstream work
Part of the Cirreum.Kernel 2.0.0 wave. Cirreum.Runtime.AuthenticationProvider takes a major
alongside it, removing a constant that duplicated a Kernel literal and renaming an instrument —
auth_transformations_total → cirreum.authn.transformations.
See also
Release v1.4.2
Release v1.4.1
Release v1.4.0
Cirreum.AuthenticationProvider 1.4.0 — Boundary resolution moves to the Kernel
IAuthenticationBoundaryResolver and DefaultAuthenticationBoundaryResolver are
removed from this package; they now live in Cirreum.Kernel 1.2.0, namespace
Cirreum.Security, beside the AuthenticationBoundary enum, IUserState, and
UserStateBase they operate on. The never-called AddAuthenticationBoundaryResolver
extension is deleted.
Why this release exists
The boundary seam is spine infrastructure: the server user-state pipeline resolves it
per invocation and grant providers consume the stamped classification, whether or not
any authentication scheme is composed. Hosting it here forced the services spine to
reference this package for exactly one interface, and left the seam's registration
orphaned — nothing called the "spine registration" extension, so every caller
classified as None. Registration is now owned by the packages that need it:
Cirreum.Services.Server registers the default alongside its user-state accessor,
and Cirreum.Runtime.Authentication registers the scheme-aware primary-scheme
resolver it restores in 1.2.0.
Migrating
Implementers of a custom resolver change one line:
// before
using Cirreum.AuthenticationProvider.Security;
// after
using Cirreum.Security; // Cirreum.KernelCompatibility
- Type removal in a minor — the same deliberate, recorded SemVer deviation as 1.3.0,
while the authentication rewrite completes: the removed types were unreachable in
practice (no registration path existed), so no correct program consumed them from
here. - Coordinated upgrade:
Cirreum.Kernel1.2.0 provides the relocated types;
Cirreum.Services.ServerandCirreum.Runtime.Authenticationpick up the new
registrations in their next releases.
See also
Cirreum.Kernel1.2.0 — the seam's new homeCirreum.Runtime.Authentication1.2.0 — restored scheme-aware classification
Release v1.3.0
Cirreum.AuthenticationProvider 1.3.0 — Container-owned audience routing data
Audience-based schemes now contribute their audience → scheme routing entries to the
service collection as immutable records, replacing a shared mutable map whose delivery
mechanism was defective under real compositions. This release is the contracts half of
a coordinated fix with Cirreum.Runtime.Authentication 1.2.0; together they repair
multi-provider audience dispatch, where only the last-registered audience instance was
actually routable.
Minor, with one deliberate deviation: two defective types are removed outright (see
Compatibility).
Why this release exists
Audience-based schemes (Entra, generic OIDC) share one credential carrier —
Authorization: Bearer — so per-request dispatch is data-driven: a shared selector
matches the token's aud claim against routing data contributed by every audience
instance. That data was delivered by mutating one shared IAudienceSchemeMap instance
that registrars located by scanning the service collection.
Instance identity turned out to be load-bearing, and it broke: under the umbrella's
composition the reuse check never succeeded, so every audience instance registration
silently created a fresh map, and last-wins DI resolution kept only the final one.
In any app with more than one audience instance, every earlier audience vanished from
dispatch — those bearer tokens were rejected fail-closed as Cirreum.Ambiguous with
no indication why. Which instance survived depended on config-key ordering. The same
instance-identity coupling also meant an application registering its own map — by
implementation type, the idiomatic shape — could never work.
What's new
AudienceSchemeRegistration
public sealed record AudienceSchemeRegistration(
string Audience,
string Scheme,
string ProviderName);The routing contribution. AudienceAuthenticationProviderRegistrar registers one per
enabled instance directly into the service collection — the same additive-set mechanic
as ISchemeSelector. There is no shared mutable state and no descriptor scanning on
the runtime path: the selector in Cirreum.Runtime.Authentication resolves
IEnumerable<AudienceSchemeRegistration> and builds its index once, at construction.
Applications integrating an IdP outside a Cirreum provider family can contribute a
mapping directly:
services.AddSingleton(new AudienceSchemeRegistration(
"api://my-legacy-api", "legacyBearer", "MyApp"));Collection-scoped duplicate-instance guard
The registrar base's duplicate-instance-key guard was process-global static state: a
second host composed in the same process — the integration-test norm — was rejected
for re-using an instance key its own composition had never seen. Guard state now lives
in the service collection, so hosts in one process are fully isolated.
Compatibility
IAudienceSchemeMapandDefaultAudienceSchemeMapare removed. Strictly this
is a breaking change shipped in a minor — a deliberate, recorded deviation. The
types were dormant-defective: the framework's own delivery path could not populate
them correctly, and no correct program could have consumed or substituted them.
Custom audience-routing semantics belong on theISchemeSelectorseam, which has
always been the dispatch extension point.- Coordinated upgrade required:
Cirreum.Runtime.Authentication1.2.0 consumes
the registration set; earlier umbrella versions reference the removed map and must
be upgraded together. - No new dependencies.
See also
Cirreum.Runtime.Authentication1.2.0 — the consuming selector, composition-close
conflict validation, and the no-match diagnostics that make audience rejections
self-explanatory