Quarkus OpenID SSF

Today the project ships a single extension — the receiver. Sibling extensions (transmitter etc.) will land alongside as they’re built.

Receiver

Lets a Quarkus app act as an SSF receiver against any spec-compliant transmitter — public providers like caep.dev, on-prem / self-hosted IdPs, or custom implementations.

What it does

  • Accepts inbound SETs via PUSH (RFC 8935) on a Vert.x route at /ssf/push.

  • Pulls SETs via POLL (RFC 8936) on a periodic Vert.x timer, with a manual pollNow() entry point for app-driven cadence.

  • Verifies every SET: JWS signature against the transmitter’s JWKS plus iss / iat / jti / aud checks per RFC 8417. RS256-only + 2048-bit minimum RSA key by default (CAEP Interop §3.1) — both knobs are config-tunable.

  • Manages stream lifecycle in two modes:

    • RECEIVER (default) — extension calls the transmitter’s configuration_endpoint on startup to discover an existing stream or create a new one.

    • TRANSMITTER — operator pre-creates the stream and provides a stream-id.

  • Exposes SsfStreamClient for the full §8.1.x management surface (read / create / patch / replace / delete config, status read+update, add/remove subjects, request verification, POLL).

  • Optional Micrometer metrics + per-event-type counters.

  • Outbound auth: static bearer token, self-contained OAuth2 client_credentials (no extra dep), quarkus-oidc-client, or no-op.

Installation

Add the extension to your Quarkus application:

<dependency>
    <groupId>io.quarkiverse.openid-ssf</groupId>
    <artifactId>quarkus-openid-ssf-receiver</artifactId>
    <version>999-SNAPSHOT</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.

  1. Sign in at https://ssf.caep.dev and copy the access token.

  2. Open https://caep.dev/transmitter/events, set the audience and start the transmitter session.

  3. 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 \
        -Dquarkus.openid-ssf.receiver.delivery-method=POLL \
        -Dquarkus.openid-ssf.receiver.poll.interval=5s
  4. Fire an event from the caep.dev transmitter tab.

  5. Inspect curl -s localhost:28080/events/recent-events | jq.

See the repository README for the full walkthrough, including Keycloak setup, the consumer SPI (SsfEventHandler), the usage-pattern catalogue, and operations notes.

Consumer SPI

Provide a CDI bean implementing SsfEventHandler:

@ApplicationScoped
public class MyHandler implements SsfEventHandler {
    @Override
    public void handle(SsfEventContext eventContext) {
        if (eventContext.hasEvent("CaepSessionRevoked")) {
            // … invalidate sessions for eventContext.eventToken().subjectId() …
        }
    }
}

Without an explicit handler, the default LoggingSsfEventHandler logs jti / iss / event-type aliases at INFO.

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 false, all startup observers (validator, push route registration, transmitter probe, receiver-managed registrar, POLL scheduler) become no-ops and no inbound SETs are accepted or pulled. Other beans (SsfStreamClient, metrics, alias resolver) stay wired in CDI so application code that touches them directly still works — but nothing happens automatically.

Useful for %test.quarkus.openid-ssf.receiver.enabled=false or runtime kill-switch via env var. Defaults to true so existing apps upgrade without behavior change.

Note: transmitter-issuer() is still resolved by the config layer at startup, so even when disabled, set it to any non-empty URI (the value is never read in disabled mode).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_ENABLED

boolean

true

Issuer URL of the SSF transmitter (e.g. a Keycloak realm URL).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_TRANSMITTER_ISSUER

URI

required

Expected aud value on inbound SETs. If set, the SET must contain a matching audience entry — otherwise it is rejected with 400. If absent, no audience check is performed.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_EXPECTED_AUDIENCE

string

Who owns the stream lifecycle. StreamManagement#RECEIVER (the default) means the extension performs a discover-or-create on startup — see ReceiverManaged — and is the most common shape for SSF receivers in practice. StreamManagement#TRANSMITTER means the operator pre-creates the stream in the transmitter’s admin console and pins the assigned stream-id() here.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_STREAM_MANAGEMENT

transmitter, receiver

receiver

Stream id pinned by the operator. Required when stream-management() is StreamManagement#TRANSMITTER; optional in StreamManagement#RECEIVER mode (if set, the registrar skips its discover-or-create step and uses this id directly).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_STREAM_ID

string

How SETs are delivered. DeliveryMethod#PUSH (the default, RFC 8935) — the transmitter POSTs each SET to Push#endpointPath(). DeliveryMethod#POLL (RFC 8936) — the extension pulls SETs from the transmitter’s poll endpoint; see Poll.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_DELIVERY_METHOD

push, poll

push

Path of the push endpoint, relative to quarkus.http.root-path.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_PUSH_ENDPOINT_PATH

string

/ssf/push

If set, the push endpoint requires this exact value in the inbound Authorization header.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_PUSH_EXPECTED_AUTH_HEADER

string

Externally-reachable URL of the push endpoint, advertised to the transmitter as delivery.endpoint_url in the create-stream request. Required when stream-management=RECEIVER; ignored otherwise.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_PUSH_DELIVERY_ENDPOINT_URL

URI

Poll endpoint URL. Optional override; if absent, the extension reads it from the stream’s delivery.endpoint_url via the configuration_endpoint on startup. Per RFC 8936, this URL is advertised by the transmitter — receivers normally don’t pin it.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_POLL_ENDPOINT_URL

URI

If true (the default), the poller schedules a periodic Vert.x timer on startup. Set to false to keep the poller idle — application code drives polling explicitly via SsfPoller.pollNow() (e.g. behind a REST endpoint, a scheduled job, or a Kafka consumer trigger).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_POLL_AUTO_START

boolean

true

Delay before the first poll fires after startup. Useful when other components need to warm up first (DB pool, config server, …). Defaults to 0s — poll immediately.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_POLL_START_DELAY

Duration 

0S

How often to poll the transmitter. Defaults to 30s.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_POLL_INTERVAL

Duration 

30S

maxEvents parameter sent on each poll request (RFC 8936 §2.1). Defaults to 100.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_POLL_MAX_EVENTS

int

100

returnImmediately parameter (RFC 8936 §2.1). When false, the transmitter holds the request open until events are available (long-poll). Defaults to true.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_POLL_RETURN_IMMEDIATELY

boolean

true

If true and the transmitter sets moreAvailable=true, keep polling synchronously until the queue drains rather than waiting for the next periodic tick. Defaults to true.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_POLL_DRAIN_ON_POLL

boolean

true

Connect/read timeout for outbound poll requests. Defaults to 30s.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_POLL_TIMEOUT

Duration 

30S

If true, the extension performs a discover-or-create dance on app startup: it lists the receiver’s existing streams on the transmitter (§7.1.1.2 with no stream_id), reuses one whose delivery.endpoint_url (or audience) matches this receiver, and creates a new stream if none matches. Defaults to true.

Set to false to manage stream lifecycle entirely from application code (e.g. via SsfStreamClient.createStream).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_RECEIVER_MANAGED_REGISTER_STREAM

boolean

true

If true, the extension issues a stream-delete call against the transmitter on app shutdown so the transmitter doesn’t accumulate stale stream registrations from short-lived receivers (dev mode, tests, …). Defaults to false so a normal restart keeps the existing stream.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_RECEIVER_MANAGED_DELETE_ON_SHUTDOWN

boolean

false

Optional human-readable description sent in the create-stream request.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_RECEIVER_MANAGED_DESCRIPTION

string

Master switch for the jti dedup layer. When true (the default), the extension consults SsfJtiDedupStore.seenBefore(jti) before invoking the application’s SsfEventHandler on both PUSH and POLL paths; duplicates are silently dropped (still 202 on PUSH, still acked on POLL — the SET was successfully received).

Set to false when the application’s handler is naturally idempotent (e.g. natural-key upsert into a database) and the extra lookup is wasted work.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_DEDUP_ENABLED

boolean

true

Capacity of the default in-memory dedup store. Overflow evicts the oldest jti. Tune to roughly cover the longest expected redelivery window — defaults to 10_000, which on a typical SSF stream (a few events per second peak) is several minutes of memory.

Ignored by custom SsfJtiDedupStore implementations.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_DEDUP_CAPACITY

int

10000

Allowed JWS alg values on inbound SETs. Any SET whose header advertises an algorithm outside this list is rejected before signature verification — defense against alg-substitution attacks. Default is [RS256] (CAEP Interop §3.1).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_SET_VALIDATION_ACCEPTED_ALGORITHMS

list of string

RS256

Minimum RSA key size, in bits, accepted for SET signature verification. SETs signed with an RSA JWK whose modulus is shorter than this are rejected. Default is 2048 (CAEP Interop §3.1). Set to 0 to disable the check (not recommended).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_SET_VALIDATION_MIN_RSA_KEY_SIZE

int

2048

If true (the default), on startup the extension fetches the configured stream’s configuration and status from the transmitter and logs a one-line summary — useful for confirming the stream exists and is enabled. Probe failures are warnings, never fatal.

Set to false when the receiver doesn’t have outbound credentials configured (push-only with public JWKS), or to keep boot silent.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_TRANSMITTER_MANAGED_PROBE_ON_STARTUP

boolean

true

Explicit URL of the transmitter’s SSF metadata document.

If unset, the URL is derived from transmitter-issuer() per SSF spec §7.2 — /.well-known/ssf-configuration is inserted between the host and the path of the issuer (NOT appended to the end). For example:

Set this property explicitly when the transmitter doesn’t follow the SSF rule — for example, OIDC-derived transmitters that serve the document at the OIDC-style appended path (<issuer>/.well-known/ssf-configuration) instead.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_TRANSMITTER_METADATA_URL

URI

JWKS URL of the transmitter. Defaults to the jwks_uri advertised in transmitter metadata.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_TRANSMITTER_JWKS_URL

URI

Static bearer access token to send on outbound calls to the transmitter. If set, the deployment processor registers a StaticTransmitterTokenProvider instead of the OIDC-backed one — useful for transmitters such as caep.dev that issue long-lived bearer tokens out-of-band rather than via an OAuth grant. Mutually exclusive with quarkus-oidc-client; when both are configured this token wins.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_TRANSMITTER_ACCESS_TOKEN

string

Event types this receiver wants to subscribe to. Required when stream-management() is StreamManagement#RECEIVER (sent in the events_requested field of the create-stream request). Informational for transmitter-managed streams — the transmitter (e.g. Keycloak admin) already controls the actual subscription set.

Each entry can be a full URI (e.g. https://schemas.openid.net/secevent/caep/event-type/session-revoked) or a short alias registered with event-aliases() event-aliases — including the SSF / CAEP / RISC built-ins (e.g. CaepSessionRevoked, RiscAccountDisabled). Resolution is performed by io.quarkiverse.ssf.receiver.runtime.event.SsfAliases#resolveEventTypeRef(String) at startup; an unregistered name fails fast with a list of available aliases.

Example:

quarkus.openid-ssf.receiver.events-requested=CaepSessionRevoked,CaepCredentialChange,\
    https://schemas.example.org/vendor/event-type/x

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_EVENTS_REQUESTED

list of string

Short aliases for event-type URIs — used as the event tag value on the ssf.receiver.events.processed meter and as accepted input to events-requested(). Keyed by alias, valued by URI:

quarkus.openid-ssf.receiver.event-aliases.VendorWidgetReplaced=https://schemas.example.org/vendor/event-type/widget-replaced

Built-in aliases for the OpenID SSF, CAEP 1.0, and RISC 1.0 spec event types are always registered out of the box (e.g. SsfStreamVerification, CaepSessionRevoked, RiscAccountDisabled, …); user entries with the same URI override the built-in alias name. Unknown URIs fall back to the URI itself as the tag value.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_EVENT_ALIASES__EVENT_ALIASES_

Map<String,URI>

Short aliases for transmitter issuer URLs — used as the iss tag value on the ssf.receiver.events.processed meter. Keyed by alias, valued by issuer URL:

quarkus.openid-ssf.receiver.issuer-aliases.KeycloakSsfPoc=https://id.localhost/realms/ssf-poc
  quarkus.openid-ssf.receiver.issuer-aliases.CaepDev=https://ssf.caep.dev

No built-in defaults — issuer URLs are deployment-specific. Unknown URLs fall back to the URL itself as the tag value.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_ISSUER_ALIASES__ISSUER_ALIASES_

Map<String,URI>

Short, stable name for this receiver — surfaced as the receiver tag on the ssf.receiver.events.processed meter so multiple receiver instances scraping into the same monitoring store stay distinguishable.

Falls back to expected-audience() if unset, then to "unknown".

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_ALIAS

string

Token endpoint URL where the OAuth2 grant is exchanged. Setting this activates the OAuth2 provider; leaving it empty falls through to the OIDC / no-op providers.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_TOKEN_ENDPOINT

URI

OAuth2 grant type sent in the grant_type form parameter. Default is client_credentials (RFC 6749 §4.4) — the only grant the receiver needs for outbound transmitter calls. Override for non-standard or extension grants (e.g. urn:ietf:params:oauth:grant-type:jwt-bearer).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_GRANT_TYPE

string

client_credentials

Which client-authentication method to use when sending the client-id() / client-secret() pair. RFC 6749 §2.3.1 defines two:

  • basic — HTTP Basic header (Authorization: Basic base64(client_id:client_secret)). RECOMMENDED by the spec and the default here.

  • post — credentials in the form body (client_id=…&client_secret=…). Some servers (notably caep.dev and various older IdPs) only accept this form.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_CLIENT_AUTH_METHOD

basic, post

basic

OAuth2 client identifier. Sent in the client_id form parameter when client-auth-method() is post, or as the basic-auth username when basic.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_CLIENT_ID

string

OAuth2 client secret. Sent in the client_secret form parameter when client-auth-method() is post, or as the basic-auth password when basic.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_CLIENT_SECRET

string

Optional space-separated scope request parameter. List form in config (e.g. quarkus.openid-ssf.receiver.oauth2.scopes=ssf.read,ssf.manage); the values are joined with single spaces on the wire.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_SCOPES

list of string

Extra form parameters appended verbatim to the token-endpoint POST. Escape hatch for server-specific extensions (e.g. resource= on Microsoft Entra, vendor-specific tenant identifiers, etc.).

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_ADDITIONAL_PARAMS__ADDITIONAL_PARAMS_

Map<String,String>

Subtracted from the token endpoint’s reported expires_in before the provider treats a cached token as expired. Default is 30s, which covers typical clock skew + the duration of the outbound call the token is about to authenticate.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_EXPIRY_SAFETY_WINDOW

Duration 

30S

Connect / read timeout for the token endpoint POST. Default is 5s.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OAUTH2_TIMEOUT

Duration 

5S

Maximum time to wait when fetching an access token from OidcClient.getTokens() for outbound calls to the transmitter. Bounds the Uni.await().atMost(…​) on the call site so a slow / unresponsive OIDC token endpoint doesn’t stall a Vert.x event loop indefinitely. Defaults to 2s.

Environment variable: QUARKUS_OPENID_SSF_RECEIVER_OIDC_TOKEN_TIMEOUT

Duration 

2S

About the Duration format

To write duration values, use the standard java.time.Duration format. See the Duration#parse() Java API documentation for more information.

You can also use a simplified format, starting with a number:

  • If the value is only a number, it represents time in seconds.

  • If the value is a number followed by ms, it represents time in milliseconds.

In other cases, the simplified format is translated to the java.time.Duration format for parsing:

  • If the value is a number followed by h, m, or s, it is prefixed with PT.

  • If the value is a number followed by d, it is prefixed with P.