Abblix.SecurityEvents 2.4.0

Prefix Reserved
dotnet add package Abblix.SecurityEvents --version 2.4.0
                    
NuGet\Install-Package Abblix.SecurityEvents -Version 2.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Abblix.SecurityEvents" Version="2.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Abblix.SecurityEvents" Version="2.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Abblix.SecurityEvents" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Abblix.SecurityEvents --version 2.4.0
                    
#r "nuget: Abblix.SecurityEvents, 2.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Abblix.SecurityEvents@2.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Abblix.SecurityEvents&version=2.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Abblix.SecurityEvents&version=2.4.0
                    
Install as a Cake Tool

Abblix.SecurityEvents

Security Event Tokens (RFC 8417) and Subject Identifiers (RFC 9493) for .NET.

It also carries the receiving half of OpenID Connect Back-Channel Logout 1.0, which is the shortest reason to install it: an application that only wants to be told its user has signed out elsewhere needs the token rules and nothing about streams. See the wiring below, and Abblix.SecurityEvents.MinimalAPI for the endpoint that receives it.

Shared Signals in .NET: SSF, CAEP, RISC and Back-Channel Logout is the map this package sits on: why the envelope came before the streams, and why nothing here has a stream concept even though it carries both delivery methods.

Install

dotnet add package Abblix.SecurityEvents

Building a Security Event Token

A SET is a JWT whose claims describe a security event. The builder enforces what the specification requires - issuer, token identifier, issue time and at least one event statement - and refuses what the profile forbids: the typ header is fixed to secevent+jwt, and exp cannot be written.

RFC 8417 Section 2.2 rates exp NOT RECOMMENDED for a token that records history, and Sections 4.1 and 4.2 make omitting it one of the layers that keep a SET from being passed off as an ID or access token - defence in depth alongside explicit typing and a distinct audience, all of which this package applies.

using System.Text.Json;
using System.Text.Json.Nodes;
using Abblix.SecurityEvents;
using Abblix.SecurityEvents.Subjects;

var compact = await new SecurityEventTokenBuilder()
    .WithIssuer("https://tenant.example.com")
    .WithAudience("https://receiver.example.com/events")
    .WithJwtId(Guid.NewGuid().ToString("N"))
    .WithEvent(
        "https://tenant.example.com/events/membership-changed",
        new JsonObject
        {
            ["subject"] = JsonSerializer.SerializeToNode<SubjectIdentifier>(
                new IssSubSubject("https://account.example.com", "a3f1c9e2")),
            ["change"] = "revoked",
        })
    .SignAsync(signer);

The signer is the seam to your cryptography: ISecurityEventTokenSigner owns key and algorithm choice, and unless a token's integrity is ensured by other means, RFC 8417 requires it to be signed. Build() alone returns the typed, unsigned model for inspection.

Validating a Security Event Token

Validation is a composed profile behind one interface. The default profile runs the receiver checks in their required order - parse, the secevent+jwt type header, the absence of exp, the presence of events, the presence of the REQUIRED jti, the issuer allowlist, the signature, the audience, the issued-at freshness window, and payload deserialization into the registered models.

var result = await validator.ValidateAsync(
    compact,
    new SecurityEventTokenValidationOptions
    {
        ExpectedAudience = "https://receiver.example.com/events",
        ExpectedIssuers = ["https://tenant.example.com"],
    });

if (result.TryGetSuccess(out var validated))
{
    // validated.Token is the typed SET; validated.EventPayloads holds the deserialized
    // payload per event identifier.
}

A consumer profile edits the default steps in place through the composition cursor (services.Decompose<ISecurityEventTokenValidator>()) - inserting, replacing or removing steps without this package changing.

A profile that removes or replaces a security-critical default must say why through SecurityEventsOptions.AllowInsecureValidation(reason): the guard demands the acknowledgement when the validator is first constructed, logs it as a warning, and otherwise refuses to construct. Every door that edits the composition is inside its reach; the one thing it cannot cover is a host registering its own ISecurityEventTokenValidator after this call, which replaces the profile and the guard together - the host visibly taking ownership of validation.

Wiring into a host

services.AddSecurityEvents(options =>
{
    options.Events.Register<MembershipChangedPayload>(
        "https://tenant.example.com/events/membership-changed");
    options.SigningKeySource = _ => Task.FromResult(signingKey); // transmitters only
});
services.AddDiscoveryKeyResolution(); // receivers: each issuer names its own JWK Set in its
                                      // discovery document, so the address follows a rotation
                                      // (AddJwksKeyResolution pins one instead)
services.AddDistributedMemoryCache(); // or Redis: the replay cache rides the host's IDistributedCache
services.AddDistributedReplayCache(); // receivers: "jti" replay protection over that store,
                                     // held for SecurityEventTokenValidationOptions.ReplayRetention

A host that receives Back-Channel Logout adds the receiver for it, naming what every Logout Token must carry - the provider as the issuer, this application's client identifier as the audience:

services.AddBackChannelLogoutReceiver(new BackChannelLogoutValidationOptions
{
    ExpectedIssuers = ["https://provider.example.com"],
    ExpectedAudience = "this-application",
});

The replay check the specification leaves optional is taken up here, because the request carrying the token is unauthenticated and the token is a bearer credential; it rides the same replay cache registered above, and a deployment wanting a strictly atomic reservation registers its own.

A pure receiver registers a key resolver and never configures signing; a pure transmitter does the reverse. Event registrations go through options.Events: registered event types deserialize into their payload models, unregistered ones pass through as UnknownEventPayload rather than failing. Every registration lets a host pre-registration win - with one loud exception: the event registry has exactly one door (options.Events), and a second registry instance is refused at wiring time rather than silently orphaning half the registrations.

Delivery types

The data shapes of both standard delivery methods, without their transports: the media type and error codes of push delivery (RFC 8935), and the request and response models of poll delivery (RFC 8936). The HTTP side belongs to the consumer, or to a Shared Signals package above this one.

Subject Identifiers

A Subject Identifier is a JSON object that says who or what an event is about, and says it in a way that names the identification mechanism rather than leaving it to be guessed. An email address, an issuer and subject pair, an opaque database key and a phone number can all identify the same subject, and without the format name a receiver cannot tell which mechanism it is holding.

What the subject is - a user, a mailbox, a device - stays between the transmitter and the receiver: the format never asserts it.

What is here today

Every Identifier Format in the IANA registry, as a type of its own:

Format Type Members
account AccountSubject uri
email EmailSubject email
iss_sub IssSubSubject iss, sub
opaque OpaqueSubject id
phone_number PhoneNumberSubject phone_number
did DidSubject url
uri UriSubject uri
aliases AliasesSubject identifiers

And the formats OpenID Shared Signals Framework 1.0 defines on top of that registry - the same vocabulary, with each constant's documentation naming which specification defines it:

Format Type Members
complex ComplexSubject user, device, session, application, tenant, org_unit, group, extensions
jwt_id JwtIdSubject iss, jti
saml_assertion_id SamlAssertionIdSubject issuer, assertion_id
ip-addresses IpAddressesSubject ip-addresses

Reading and writing

Serialization is polymorphic on the format member, and it needs no configuration: the converter is attached to the base type.

using System.Text.Json;
using Abblix.SecurityEvents.Subjects;

SubjectIdentifier subject = new IssSubSubject("https://issuer.example.com/", "145234573");

var json = JsonSerializer.Serialize(subject);
// {"format":"iss_sub","iss":"https://issuer.example.com/","sub":"145234573"}

var parsed = JsonSerializer.Deserialize<SubjectIdentifier>(json);
// an IssSubSubject

Reading is strict, because RFC 9493 is: a document missing a required member, carrying an empty one, or carrying a member its format does not describe is rejected with a JsonException rather than accepted and silently rewritten on the next serialization.

An aliases identifier holds several identifiers for one entity, and nesting one inside another is rejected on construction, whether the value was built in code or read off the wire:

var subject = new AliasesSubject(
    new EmailSubject("user@example.com"),
    new PhoneNumberSubject("+12065550100"));

Comparing values

Nothing is canonicalised on the way in. RFC 9493 records that email canonicalisation is not standardised and that a receiver cannot know the sending provider's algorithm, so folding a value on arrival would answer that question on the application's behalf and destroy the original.

Two transformations are offered for use at comparison time. EmailCanonicalization.ToComparableForm lowercases the domain, which is case-insensitive for every provider, and leaves the local part alone. PhoneNumberCanonicalization.ToComparableForm removes presentation characters, which E.164 does not include in a number. Neither can merge two values that are genuinely distinct.

Formats beyond the registry

A format defined by a later specification is a subclass plus one registration:

var options = new JsonSerializerOptions
{
    Converters =
    {
        new SubjectIdentifierJsonConverter(
            new Dictionary<string, Type> { ["urn:example:format"] = typeof(MyFormatSubject) }),
    },
};

A name from the built-in vocabulary - RFC 9493 or Shared Signals - cannot be rebound, so a custom format can never change how a standard document is read.

Part of the Abblix product family

The event dictionaries Abblix.SecurityEvents.CAEP and Abblix.SecurityEvents.RISC register their typed payloads over this package's event registry, and Abblix.SharedSignals carries the tokens built here over managed event streams; the full family lives in the repository.

License

Abblix.SecurityEvents is licensed under the Apache License 2.0.

Contacts

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Abblix.SecurityEvents:

Package Downloads
Abblix.OIDC.Server

OpenID Connect and OAuth 2.0 server for ASP.NET Core, certified by the OpenID Foundation. Add a complete identity provider and authorization server to your own .NET application: every OIDC flow, PKCE, PAR, DPoP, JARM, CIBA, device flow, token exchange and FAPI 2.0. Runs on .NET 8, 9 and 10.

Abblix.SharedSignals

OpenID Shared Signals Framework (SSF) 1.0 for .NET: transmitter and receiver in one package. Discover a transmitter, manage event streams and subjects, verify delivery, and send or receive CAEP and RISC events by push or poll.

Abblix.SecurityEvents.CAEP

OpenID CAEP 1.0 (Continuous Access Evaluation Profile) event dictionary for .NET: typed models for session revoked, token claims change, credential change, assurance level change, device compliance change, session established, session presented and risk level change, over Abblix Security Events.

Abblix.SecurityEvents.MinimalAPI

ASP.NET Core Minimal API integration for Abblix Security Events: receive OpenID Back-Channel Logout tokens and pushed Security Event Tokens (RFC 8935) as route handlers, with no MVC dependency.

Abblix.SecurityEvents.RISC

OpenID RISC 1.0 (Risk Incident Sharing and Coordination) event dictionary for .NET: typed models for credential compromise, account disabled, enabled and purged, identifier changed and recycled, opt-out and recovery events, over Abblix Security Events.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.4.0 226 9/5/2026

The event layer of the Shared Signals stack, usable on its own. A typed model of a Security Event Token with a builder and a composable validation profile over Abblix JWT, so a token is refused for a missing identifier, a wrong audience or a replay before any handler sees it. Every registered Subject Identifier format has a typed model with polymorphic JSON handling. Delivery models cover push, where the transmitter posts each token, and poll, where the receiver collects and acknowledges them; keys are resolved from the counterparty's JWKS. OpenID Back-Channel Logout rides on the same core, so a relying party validates logout tokens with the same code that validates any other security event. Full details: https://github.com/Abblix/Oidc.Server/releases/tag/v2.4