Abblix.SharedSignals.MinimalAPI 2.4.0

Prefix Reserved
dotnet add package Abblix.SharedSignals.MinimalAPI --version 2.4.0
                    
NuGet\Install-Package Abblix.SharedSignals.MinimalAPI -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.SharedSignals.MinimalAPI" Version="2.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Abblix.SharedSignals.MinimalAPI" Version="2.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Abblix.SharedSignals.MinimalAPI" />
                    
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.SharedSignals.MinimalAPI --version 2.4.0
                    
#r "nuget: Abblix.SharedSignals.MinimalAPI, 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.SharedSignals.MinimalAPI@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.SharedSignals.MinimalAPI&version=2.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Abblix.SharedSignals.MinimalAPI&version=2.4.0
                    
Install as a Cake Tool

Abblix.SharedSignals.MinimalAPI

ASP.NET Core Minimal API integration for Abblix.SharedSignals: the OpenID Shared Signals Framework 1.0 endpoints as route handlers, with no MVC dependency.

Shared Signals in .NET: SSF, CAEP, RISC and Back-Channel Logout explains which endpoints belong to the event layer and which to the stream layer, so the two adapters stop looking interchangeable.

Which adapter maps which endpoint

There are two Minimal API packages in this family, and the line between them is not the one that first suggests itself. It is not transmitter here and receiver there: this package holds receiver-role code of its own - the stream management client, the transmitter discovery client. The question that decides placement is whether the endpoint stops making sense without a stream:

  • Stream management, status, subjects, verification, the ssf-configuration document, and the transmitter's poll endpoint: every one is meaningless without a stream, and the poll address is addressed by stream identifier. Here.
  • Push delivery intake: "accept a SET at this address" (RFC 8935 Section 2.1). The URL is the receiver's own and carries no stream identity, so a receiver can be handed events by a counterparty known from anywhere. That endpoint is MapPushDeliveryEndpoint in Abblix.SecurityEvents.MinimalAPI, which a push-based receiver installs alongside this one: the dependency chain here reaches the core library but not the core's adapter.
  • Back-Channel Logout: one token, delivered once, from a provider the relying party already knows. Also there.

Push and poll are the pair worth understanding, because both are core delivery specifications (RFC 8935 and RFC 8936) and yet they land in different packages. What separates them is not which document defines the protocol but whether a stream is part of the addressing: the push intake just accepts a token, while the poll endpoint below serves one stream's queue and is addressed per stream. The specification says how to carry an event; the stream says to whom - and that second half is what this package is.

The split is kept for what it buys the other side: a relying party that wants only Back-Channel Logout takes the core adapter alone, and never sees this package's surface.

Install

dotnet add package Abblix.SharedSignals.MinimalAPI
dotnet add package Abblix.SecurityEvents.MinimalAPI   # a push-based receiver also needs the intake endpoint

Transmitter

builder.Services
    .AddSecurityEvents(options => options.SigningKeySource = ...)
    .AddSharedSignalsTransmitter(new SharedSignalsTransmitterOptions
    {
        Issuer = "https://tr.example.com",
        EventsSupported = ["https://schemas.openid.net/secevent/caep/event-type/session-revoked"],
        JwksUri = new Uri("https://tr.example.com/.well-known/jwks.json"),

        // A receiver following the CAEP Interoperability Profile adds no subjects, so a transmitter
        // targeting that profile covers them all. Leave it out and new streams cover nothing until a
        // subject is named - which the startup log says, because neither side would.
        DefaultSubjectsMode = StreamSubjectsMode.All,
    });

var app = builder.Build();
app.MapSharedSignalsTransmitterEndpoints().RequireAuthorization("ssf-receivers");

One call maps the whole management surface under SharedSignalsEndpointOptions.ManagementPrefix (/ssf by default) - streams, status, subjects, verification, poll delivery - plus the configuration document at the well-known address the issuer resolves to. Every route comes from the options, so one object states the whole topology.

A route you add to that group yourself is not scope-checked. The filter is attached to the group, so it is in your route's pipeline - but it judges a route by the scope requirement the route declares, and only the routes mapped by the call above declare one. A route with none is let through, which means your own route beside them is admitted for any caller your authorization admits, in a deployment where every neighbouring route answers 403 to that same caller.

Attach your own authorization to that route. The scopes themselves are public - SsfScopes carries their names and the profile's inclusion rule, and it ships in the package this one depends on - so what you cannot declare is the requirement metadata, not the vocabulary.

The well-known endpoint stays outside the returned group on purpose: discovery must answer before any receiver has credentials, so the authorization you attach to the group does not cover it. What it serves is public metadata - issuer, JWKS location, endpoint addresses, supported delivery methods and authorization schemes - and nothing stream- or receiver-specific; poll delivery sits inside the group, which is where SSF 1.0 Section 7.1.1 wants it.

Two members of that metadata come from defaults. authorization_schemes is published as OAuth 2.0 unless you set it - the CAEP Interoperability Profile requires that value, and it is a claim about how your management surface is authorized, so a deployment using something else should set AuthorizationSchemes to its own list or to an empty list to advertise none. jwks_uri has no default, because only you know where your JWK Set is published, and without it no receiver can verify an event. Both absences are logged once when the routes are mapped.

A gateway-fronted deployment adjusts this in the same options object, without moving the protocol address: MapWellKnownConfiguration = false leaves the canonical route to the gateway or CDN in front, ConfigurationDocumentRoute names the internal route a rewriting proxy maps the canonical address onto (served by MapSharedSignalsConfigurationDocument()), and AdvertisedPrefix is what the document advertises - the external prefix, whatever ManagementPrefix mapped internally. The external address never moves, because receivers derive it from the issuer.

Receivers are told apart by identity: the endpoints read it from the authenticated principal (the sub claim, then the identity name), and SharedSignalsEndpointOptions.ReceiverIdSelector replaces that mapping when the host's authentication carries the identity elsewhere.

Scopes are the other half, and they are off until you switch them on. The CAEP Interoperability Profile defines ssf.read and ssf.manage and requires a transmitter to check that a token is sufficient for what was asked. It assigns five operations: reading a stream's configuration and getting its status to ssf.read, creating a stream, deleting one and verification to ssf.manage. The other six routes it does not assign, and this library places them - everything that changes a stream needs ssf.manage, and poll needs ssf.read.

Note what that last one costs: a poll acknowledges, acknowledging releases the transmitter from retaining those events, so ssf.read is enough to empty a queue - and while a stream is looked up by the caller's identity, the queue behind it is keyed by stream id alone, so two receivers naming one stream share it.

Each route knows which scope it needs, but this package never sees a token, so it cannot find the granted scopes on its own:

builder.Services.AddSingleton(new SharedSignalsEndpointOptions
{
    GrantedScopesSelector = ctx => ctx.User.FindFirst("scope")?.Value.Split(' ') ?? [],
});

Set it and a caller whose token is too narrow gets 403 with insufficient_scope and the scope it needs to ask for. A caller nothing identified still gets the bare 401 instead - not having authenticated is not a scope problem. Leave the selector unset and no scope is checked at all, which is what this surface did before the option existed: a working deployment, and one outside the profile. Your authorization server has to be able to grant the two scope values; this library's own refuses any scope nobody registered with it.

What one call maps, relative to the prefix:

Route Method SSF 1.0
/stream POST, GET, PATCH, PUT, DELETE stream management, Section 8.1.1
/status GET, POST stream status, Section 8.1.2
/subjects:add, /subjects:remove POST subject management, Section 8.1.3
/verify POST verification request, Section 8.1.4
/poll/{streamId} POST poll delivery, RFC 8936

The configuration document at /.well-known/ssf-configuration advertises the five management addresses from the very constants that map them, so those cannot drift; the well-known path itself follows the specification, not the prefix, because that fixed address is how a receiver holding only the issuer URI finds everything else.

The poll address travels per stream rather than in the document, and it comes from the same prefix, so a stream's endpoint_url leads back to the route serving it wherever you map the prefix. A proxy that rewrites paths needs nothing extra: what the mapping declares is AdvertisedPrefix, so the poll address follows it along with the five above. PollEndpointFactory is for the address that prefix cannot describe - delivery on a separate host name, say - and it wins.

Receiver

builder.Services
    .AddSecurityEvents()
    .AddJwksKeyResolution()
    .AddDistributedMemoryCache()   // or Redis - the replay cache rides the host's IDistributedCache
    .AddDistributedReplayCache()
    .AddSharedSignalsReceiver(new SharedSignalsValidationOptions
    {
        ExpectedAudience = "https://receiver.example.com",
        ExpectedIssuers = ["https://tr.example.com"],
        StreamIssuer = "https://tr.example.com",
    })
    .AddSingleton<ISecurityEventSink, MyEventSink>();

var app = builder.Build();
app.MapPushDeliveryEndpoint("/events")
    .RequireAuthorization("ssf-transmitters");

The intake endpoint itself comes from Abblix.SecurityEvents.MinimalAPI, which this package already depends on - see the section above for why RFC 8935's intake belongs to the core while the poll endpoint above belongs here. It answers the empty 202 or the 400 whose body speaks the RFC 8935 registry vocabulary; where accepted events land is the host's ISecurityEventSink.

Signature validation decides whether an event is genuine, but it is not what keeps the endpoint standing: an unauthenticated route spends a cryptographic verification on every body posted to it, which RFC 8935 Section 5.4 names as the recipient's denial-of-service exposure and answers with transmitter authentication and rate limiting. Attach both - the returned builder takes the host's conventions like any other route.

Part of the Abblix product family

Abblix.SharedSignals.MinimalAPI maps the routes of Abblix.SharedSignals, which in turn sits on Abblix.SecurityEvents. The identity provider these signals originate from is Abblix OIDC Server.

License

See LICENSE.md.

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.4.0 43 9/5/2026

The Shared Signals endpoints as route handlers. For a transmitter: configuration discovery, stream management, subject management, verification and poll delivery. For a receiver: the push intake a transmitter posts to. Each validates through the Shared Signals and Security Events cores before handing on, declares the statuses it answers, and needs no MVC framework in the host. Full details: https://github.com/Abblix/Oidc.Server/releases/tag/v2.4