Quarkus OpenID SSF
A Quarkus extension family for the OpenID Shared Signals Framework (SSF) and its companion specs (CAEP, RISC, RFC 9967 SCIM Events, RFC 8417 Security Event Tokens, RFC 8935 PUSH delivery, RFC 8936 POLL delivery, CAEP Interoperability Profile 1.0 - draft 02).
Today the project ships a single extension — the SSF receiver.
Receiver
Lets a Quarkus app act as an SSF receiver against any spec-compliant transmitter, to integrate with public providers like caep.dev, on-prem / self-hosted IdPs, or custom implementations.
What it does
The protocol work is done by easyssf
(org.easyssf:easyssf-receiver), a framework independent SSF receiver library
shared with its Spring Boot starter. The extension adds configuration, CDI
wiring, the Vert.x push route, the poll scheduler, the token providers,
Micrometer, a health check, JDBC, the Dev UI and native image support.
-
Accepts inbound SETs via PUSH (RFC 8935) on a Vert.x route at
/ssf/push, answering with the RFC 8935 error documents; handlers run before the202. -
Pulls SETs via POLL (RFC 8936) with a poller per transmitter on a virtual thread, periodic or long polling, acknowledgements that wait in a store (optionally the database) until a request carried them, error reports,
Retry-Afterhandling and a manualpollNow(). -
Verifies every SET: JWS signature against the transmitter’s JWK Set,
typheader,iss/iat/jti/aud/eventsper RFC 8417. RS256-only with a 2048-bit minimum RSA key by default (CAEP Interop Profile), tunable. -
Manages the stream of the receiver:
RECEIVER(default) looks it up at the transmitter on startup and creates or updates it, with retries in the background;TRANSMITTERuses a stream an operator created. -
Receives from several transmitters (
quarkus.openid-ssf.receiver.<name>.*), selected by the issuer of a SET. -
Exposes the stream management API (SSF section 8.1) through
SsfReceiverStreamClientand easyssf’sSsfStreamClient. -
Optional Micrometer metrics (
easyssf.receiver.*), a SmallRye Health wellness check, JDBC stores over Agroal (processed SETs, pending poll acknowledgements) shared by all instances, a Dev UI. -
Outbound auth: static bearer token, OAuth2
client_credentials(no extra dependency),quarkus-oidc-client, or none.
Installation
Add the extension to your Quarkus application:
<dependency>
<groupId>io.quarkiverse.openid-ssf</groupId>
<artifactId>quarkus-openid-ssf-receiver</artifactId>
<version>0.6.0</version>
</dependency>
Quick start
The fastest end-to-end smoke test is against the public caep.dev transmitter — no Keycloak setup, no public tunnel, and no admin server of your own.
-
Sign in at https://ssf.caep.dev and copy the access token.
-
Open https://caep.dev/transmitter/events, set the audience and start the transmitter session.
-
Run the receiver-managed example in POLL mode:
export SSF_RECEIVER_TRANSMITTER_ACCESS_TOKEN=<your-caep-dev-token> export SSF_RECEIVER_EXPECTED_AUDIENCE=https://my-receiver.example/ssf mvn -pl receiver/examples/example-receiver-managed-stream quarkus:dev \ -Dquarkus.profile=caepdev,poll \ -Dquarkus.openid-ssf.receiver.poll.interval=5s -
Fire an event from the caep.dev transmitter tab.
-
Inspect
curl -s localhost:28080/events/recent-events | jq.
See the repository README for the full walkthrough, and the examples for a complete setup with Keycloak as identity provider and transmitter.
Consumer SPI
Provide a CDI bean implementing easyssf’s SsfEventHandler; every handler bean
is invoked for every verified SET, before the transmitter gets its answer:
import org.easyssf.receiver.event.SsfEventContext;
import org.easyssf.receiver.event.SsfEventHandler;
@ApplicationScoped
public class MyHandler implements SsfEventHandler {
@Override
public void handle(SsfEventContext eventContext) {
if (eventContext.hasEvent("CaepSessionRevoked")) {
SsfSubject subject = eventContext.subjectFor("CaepSessionRevoked");
// … invalidate the session subject.sessionId() of subject.subject() …
}
}
}
A handler that throws makes the transmitter deliver the SET again, so handlers
have to be idempotent. Without an explicit handler, the default
LoggingSsfEventHandler logs jti / iss / event-type aliases at INFO.
Testing
org.easyssf:easyssf-test has TestTransmitter, an in-process SSF transmitter
and identity provider for the tests of an application: metadata, JWK Set,
signed SETs and access tokens, a token endpoint, the stream API and a poll
endpoint. The examples show a QuarkusTestResourceLifecycleManager around it.
Architecture
For the "why is the code shaped this way" view — module layout, startup
observer priorities, hot-path flow, build-time decisions — see
design/receiver.md.
Configuration Reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
|---|---|---|
Master kill switch for the SSF receiver. When Environment variable: |
boolean |
|
Type |
Default |
|
Issuer of the SSF transmitter, for example a Keycloak realm URL. Every inbound SET must carry this value as its Environment variable: |
string |
|
Location of the transmitter’s SSF configuration metadata. If unset, the URL is derived from Environment variable: |
||
Location of the JWK Set used to verify SET signatures. Discovered from the transmitter metadata ( Environment variable: |
||
Whether the transmitter issuer and the endpoints it publishes may use plain Environment variable: |
boolean |
|
Audience this receiver is known as at the transmitter. When set, every inbound SET must contain it in its Environment variable: |
string |
|
Who manages the stream. Environment variable: |
|
|
Identifier of a stream that was created at the transmitter. When set, the stream is looked up on startup to learn its audience and poll endpoint. Environment variable: |
string |
|
How SETs are delivered. Environment variable: |
|
|
Event types this receiver wants to subscribe to, as a full URI or an alias (the built-in aliases of the SSF, CAEP, RISC and SCIM event types, Environment variable: |
list of string |
|
Static bearer access token to send on calls to the transmitter, for transmitters such as caep.dev that hand out long-lived tokens out-of-band. Takes precedence over Environment variable: |
string |
|
Exact value the transmitter must send in the Environment variable: |
string |
|
URL under which the transmitter reaches the push endpoint of this application, advertised as Environment variable: |
||
Poll endpoint of the transmitter. Taken from the configuration of the stream when not set, which needs the stream to be looked up or managed on startup. Environment variable: |
||
Whether the transmitter is polled by the extension, on a virtual thread of its own per transmitter: every Environment variable: |
boolean |
|
Time to wait before the first poll after startup. Environment variable: |
|
|
Time between two polls. With Environment variable: |
|
|
Long polling (RFC 8936, section 2.5): the poller keeps one request outstanding that the transmitter holds until SETs are available or Environment variable: |
boolean |
|
How long the transmitter holds a long poll request, part of the agreement with it (RFC 8936, section 2.2). The request waits that long plus a margin for the response, Environment variable: |
|
|
Maximum number of SETs to fetch with one request ( Environment variable: |
int |
|
Pause after a Environment variable: |
||
Longest pause a Environment variable: |
|
|
Whether the stream managed by the receiver is deleted at the transmitter when the application stops, so that short-lived receivers (dev mode, tests) do not leave streams behind. Environment variable: |
boolean |
|
Description of the stream managed by the receiver. Environment variable: |
string |
|
JWS algorithms accepted for SET signatures. Only asymmetric signature algorithms are allowed. A SET signed with another algorithm is rejected. Environment variable: |
list of string |
|
Minimum RSA key size in bits for SET signing keys. Environment variable: |
int |
|
Whether a SET must be explicitly typed with a Environment variable: |
boolean |
|
How strictly the subject of a SET is validated: Environment variable: |
|
|
Tolerated clock skew when checking that a SET was not issued in the future. Environment variable: |
|
|
Accepts SETs whatever audience they carry when neither Environment variable: |
boolean |
|
Token endpoint to obtain access tokens from with the client credentials grant. Setting it activates this provider. Environment variable: |
||
Client id of this receiver. Environment variable: |
string |
|
Client secret of this receiver. Environment variable: |
string |
|
Scopes to request, sent space-separated in the Environment variable: |
list of string |
|
How the client authenticates at the token endpoint: Environment variable: |
|
|
Form parameters to send with the token request in addition to the grant, for extensions of the token endpoint. Environment variable: |
Map<String,String> |
|
How long before its expiry an access token is renewed; never more than a quarter of the token’s lifetime. Environment variable: |
|
|
Name of the OIDC client ( Environment variable: |
string |
|
Maximum time to wait for an access token from the OIDC client. Environment variable: |
|
|
Aliases for event type URIs, usable wherever an event type is named: in
They add to the built-in aliases of the SSF, CAEP, RISC and SCIM event types (for example Environment variable: |
Map<String,String> |
|
The members of a complex subject the application interprets. A transmitter can declare members critical ( Environment variable: |
list of string |
|
Path of the push endpoint, relative to Environment variable: |
string |
|
Connect timeout for calls to the transmitters. Environment variable: |
|
|
Read timeout for calls to the transmitters: how long to wait for the response to a metadata, JWK Set, stream management, token or poll request. Environment variable: |
|
|
Environment variable: |
string |
|
Whether SETs that were already processed are skipped. A SET that was processed before is acknowledged ( Environment variable: |
boolean |
|
Number of SET identifiers the in-memory store remembers. Overflow evicts the oldest. Ignored by a JDBC store or a custom Environment variable: |
int |
|
How long the JDBC store remembers a processed SET. Has to cover the time the transmitter keeps trying to deliver a SET. Environment variable: |
|
|
How long a SET counts as being handled by the instance that claimed it without renewing the claim, which it does every third of this time while its handlers run. A SET is claimed in the store before the handlers run and marked processed afterwards; another instance that receives the same SET meanwhile neither handles nor acknowledges it, the transmitter delivers it again. A claim that was not renewed for the lease counts as abandoned by an instance that crashed, and the SET is handled again. Environment variable: |
|
|
Whether the state of the receiver is kept in the default datasource of the application, if it has one ( Environment variable: |
boolean |
|
Whether the tables are created at startup if they do not exist, and columns a release added are added to an existing table. With Environment variable: |
boolean |
|
Prefix of the names of the tables. Environment variable: |
string |
|
How often expired rows (processed SETs past Environment variable: |
|
|
How long an acknowledgement or error report the transmitter never accepted is kept in the Environment variable: |
|
|
How many acknowledgements one Environment variable: |
int |
|
|
About the Duration format
To write duration values, use the standard You can also use a simplified format, starting with a number:
In other cases, the simplified format is translated to the
|