Shared Signals Framework receivers for Java
easyssf lets your application react when your identity provider revokes a session, changes a credential or signals a risk. It verifies the security events of the OpenID Shared Signals Framework, rejects the access tokens and ends the sessions concerned, and hands everything else to your code. A Spring Boot starter and a Quarkus extension wire it into your application.
How it works
A transmitter, usually your identity provider, delivers Security Event Tokens (SETs) to your application, either by pushing them to an endpoint or by letting the application poll for them. easyssf verifies each token, skips duplicates and routes the events to the integrations that act on them.
Push or pollRFC 8935 · RFC 8936
A push endpoint on a route of your choice, secured by its own stateless filter chain, or a poller that fetches and acknowledges SETs from the transmitter.
Every SET verifiedRFC 8417
Signature against the transmitter's JWK Set, discovered from its .well-known/ssf-configuration,
plus typ, iss, aud, jti, iat and events.
The transmitter and every endpoint it publishes must use HTTPS.
Resource server integration
A CAEP session-revoked event makes the application reject the access tokens of that session,
or all tokens of the user issued before the event: built into the Spring Boot starter, a few lines with
quarkus-oidc.
OIDC client integration
session-revoked and credential-change events invalidate the matching local
sessions, by session id, subject or email.
Stream management
Let the application create or update its stream at the transmitter on startup, verify it, and use the
whole stream management API through SsfStreamClient. Several transmitters, several Keycloak
realms say, are each configured by name; the issuer of a SET selects the one that verifies it.
Production details covered
State in your database when there is one, Micrometer metrics, a health indicator for Actuator or SmallRye Health, retries with backoff, and the application starts even while the transmitter is down.
Getting started
easyssf comes as a starter for Spring Boot and, through the Quarkiverse extension quarkus-openid-ssf, as an extension for Quarkus. Both are built on the same receiver library and verify, de-duplicate and dispatch events the same way.
Three steps for a Spring Boot application with the servlet stack.
Add the starter
<dependency>
<groupId>org.easyssf</groupId>
<artifactId>easyssf-receiver-spring-boot-starter</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
No release yet: the snapshots of main are on the
Maven Central snapshot repository,
which your build has to enable.
Name the transmitter
easyssf:
receiver:
transmitter-issuer: https://idp.example/realms/demo
expected-audience: https://my-app.example
push:
expected-auth-header: Bearer ${SSF_PUSH_SECRET}
Create the stream
Create a stream with push delivery at the transmitter that points to
https://my-app.example/ssf/push and sends the configured header, or set
easyssf.receiver.stream.management: receiver and let the application create it.
With spring-boot-starter-security-oauth2-resource-server or
spring-boot-starter-security-oauth2-client on the classpath, revoked sessions are handled
from then on without further code.
React to any event
@Component
class StepUpHandler implements SsfEventHandler {
@Override
public void handle(SsfEventContext eventContext) {
if (eventContext.hasEvent("CaepAssuranceLevelChange")) {
SsfSubject subject = eventContext.subjectFor("CaepAssuranceLevelChange");
Map<String, Object> event = eventContext.eventFor("CaepAssuranceLevelChange");
// subject.subject(), subject.sessionId(), subject.email() ...
}
}
}
Handlers run before the SET is acknowledged. If one throws, the transmitter is expected to deliver the SET again.
Three steps for a Quarkus application.
Add the extension
<dependency>
<groupId>io.quarkiverse.openid-ssf</groupId>
<artifactId>quarkus-openid-ssf-receiver</artifactId>
<version>0.2.0</version>
</dependency>
Or quarkus ext add io.quarkiverse.openid-ssf:quarkus-openid-ssf-receiver.
No release yet: 0.2.0 is the first version built on easyssf and follows the easyssf 0.1.0 release.
Until then, build the extension from
source.
Name the transmitter
quarkus.openid-ssf.receiver.transmitter-issuer=https://idp.example/realms/demo
quarkus.openid-ssf.receiver.events-requested=CaepSessionRevoked,CaepCredentialChange
quarkus.openid-ssf.receiver.push.delivery-endpoint-url=https://my-app.example/ssf/push
quarkus.openid-ssf.receiver.push.expected-auth-header=Bearer ${SSF_PUSH_SECRET}
Create the stream
By default the application creates or updates its stream at the transmitter on startup and tells the transmitter to send the configured header. It authenticates at the stream management API with a client of the transmitter:
quarkus.openid-ssf.receiver.oauth2.token-endpoint=https://idp.example/realms/demo/protocol/openid-connect/token
quarkus.openid-ssf.receiver.oauth2.client-id=my-app
quarkus.openid-ssf.receiver.oauth2.client-secret=${SSF_CLIENT_SECRET}
Or set quarkus.openid-ssf.receiver.stream-management=TRANSMITTER
and the stream-id of a stream an operator created. With quarkus-oidc in the same
application, set quarkus.http.auth.proactive=false, so that the header the transmitter sends to
the push endpoint is not taken for an access token. Rejecting revoked access tokens and ending revoked sessions
take a few lines on top of SsfTokenRevocationEventHandler, a SecurityIdentityAugmentor
in a resource server and a TokenStateManager in a web application; the
examples show both.
React to any event
@ApplicationScoped
public class StepUpHandler implements SsfEventHandler {
@Override
public void handle(SsfEventContext eventContext) {
if (eventContext.hasEvent("CaepAssuranceLevelChange")) {
SsfSubject subject = eventContext.subjectFor("CaepAssuranceLevelChange");
Map<String, Object> event = eventContext.eventFor("CaepAssuranceLevelChange");
// subject.subject(), subject.sessionId(), subject.email() ...
}
}
}
Every SsfEventHandler bean is invoked for every verified SET,
before the transmitter gets its answer. If one throws, the transmitter is expected to deliver the SET again.
Subjects as the spec defines them
SsfSubject is the sub_id of the SET: a subject identifier in any RFC 9493 format
(iss_sub, email, opaque, account, phone_number,
did, uri, aliases) or a complex subject with its user,
session, device, tenant and other members. Shortcuts cover the common
cases, nothing is lost.
Your own event type aliases
The SSF, CAEP and RISC event types have built-in aliases such as CaepSessionRevoked. Register
aliases for vendor specific event types, in configuration or in code, and use them wherever an event type is
named. The URIs stay canonical; an alias can never redefine another.
Test your receiver
easyssf-test brings a transmitter that runs inside your test JVM: it signs SETs, serves metadata
and keys, hands out tokens and emulates the stream and poll endpoints. Push a revocation, assert the effect.
The tests of the Spring Boot starter and of the Quarkus extension use it.
./mvnw install and see the README for the
complete configuration reference of the Spring Boot starter, and the
README of quarkus-openid-ssf for the one
of the Quarkus extension.Modules
Everything is published under the group id org.easyssf. The Quarkus extension is a
Quarkiverse project with its own coordinates.
| Module | What it is | Depends on |
|---|---|---|
easyssf-core | The data structures of SSF shared by receivers and, later, transmitters: SETs, subjects, event types, stream configuration, transmitter metadata. | nothing |
easyssf-receiver | The receiver, independent of any framework: SET verification, de-duplication, event handlers, push handling, polling, stream management, token revocation and session termination logic. | easyssf-core, Nimbus JOSE + JWT, SLF4J |
easyssf-receiver-jdbc | The database-backed stores of the receiver, processed SETs and revocations, independent of any framework: the SQL, the schema and a small execution interface implemented over a DataSource or a framework's template. | easyssf-receiver |
easyssf-receiver-spring-boot-starter | The receiver for Spring Boot 4.1 with Spring Security 7.1 on the servlet stack: configuration properties, auto-configuration, push endpoint, resource server and OIDC client integration. | easyssf-receiver, Spring Boot |
easyssf-test | Test support: a transmitter on a loopback port that signs and delivers SETs, serves metadata and keys and emulates the stream and poll endpoints, for the tests of your receiver. | easyssf-core, Nimbus JOSE + JWT |
easyssf-receiver-spring-boot-examples | An example resource server and OIDC client with a Keycloak setup. | |
easyssf-test-conformance | Runs the OpenID conformance suite's SSF receiver test plans against a receiver under test, in any framework: the suite via Testcontainers, the scenarios the receiver plays, and the plan tests to extend. | easyssf-receiver, Testcontainers, JUnit |
easyssf-receiver-spring-boot-conformance-tests | The Spring Boot receiver under test and the four plan tests for it. | |
quarkus-openid-ssf-receiver | The receiver for Quarkus 3.27, group id io.quarkiverse.openid-ssf: configuration, CDI wiring, the Vert.x push route, the poll scheduler, token providers, Micrometer, SmallRye Health, JDBC, Dev UI and native image support. Its examples and conformance tests live in the same repository. | easyssf-receiver, easyssf-receiver-jdbc, Quarkus |
Without Spring Boot or Quarkus
easyssf-receiver has no framework dependencies. Assemble the parts you need
and call them from the endpoint or scheduler of your framework. The Spring Boot starter and the Quarkus extension
are two such integrations.
SsfHttpClient httpClient = new JdkSsfHttpClient();
String issuer = "https://idp.example/realms/demo";
SsfTransmitterMetadataResolver metadata = new SsfTransmitterMetadataResolver(issuer, null, httpClient);
NimbusSsfSetVerifier verifier = new NimbusSsfSetVerifier(issuer,
() -> metadata.resolve().jwksUri().toString(), httpClient);
verifier.setExpectedAudience("https://my-app.example");
SsfEventHandler handler = (eventContext) -> {
if (eventContext.hasEvent("CaepSessionRevoked")) {
SsfSubject subject = eventContext.subjectFor("CaepSessionRevoked");
// end the session subject.sessionId() of the user subject.subject()
}
};
SsfSetProcessor processor = new SsfSetProcessor(verifier, new InMemorySsfJtiDedupStore(10_000), List.of(handler));
// PUSH: call this from the endpoint the transmitter posts SETs to
SsfPushHandler pushHandler = new SsfPushHandler(processor, "Bearer " + pushSecret);
SsfPushResponse response = pushHandler.handle(authorizationHeader, requestBody);
// POLL: fetch SETs from the transmitter instead
SsfPoller poller = new SsfPoller(httpClient, tokenProvider, () -> pollEndpoint, processor);
poller.start();
Interoperability
Keycloak
Tested with the SSF transmitter of Keycloak 26.8 (--features=ssf). The examples ship a
pre-configured realm, and the README documents Keycloak's audience, scopes and single-stream rule.
OpenID conformance suite
The receiver test plans of the OpenID conformance suite, default and CAEP interop profile with push and poll delivery, run against the receiver in an automated test module.
Standards
Built on the OpenID Shared Signals specifications and the IETF security event RFCs, see Specifications for the complete list and what easyssf takes from each.
Not included yet: Spring WebFlux applications and long polling.
Specifications
What easyssf implements, and where each piece is defined.
| Specification | Defines | In easyssf |
|---|---|---|
| OpenID Shared Signals Framework 1.0 | Transmitters, receivers, streams, transmitter metadata discovery, stream verification | Metadata discovery, stream management and verification, the SSF event types |
| OpenID CAEP 1.0 | Continuous Access Evaluation Profile: session-revoked, credential-change, assurance-level-change and the other session and credential events |
Event type aliases, the resource server and OIDC client integrations |
| OpenID CAEP Interoperability Profile 1.0 | The minimum a CAEP transmitter and receiver must support to work together | The CAEP interop plans of the conformance tests |
| OpenID RISC Profile 1.0 | Risk Incident Sharing and Coordination: account disabled, purged, credential compromise and the other account events | Event type aliases, for your own handlers |
| RFC 8417 | Security Event Token (SET): the JWT profile every event is delivered in | Verification of signature, typ, iss, aud, jti, iat and events |
| RFC 8935 | Push-based SET delivery over HTTP | The push endpoint and its responses |
| RFC 8936 | Poll-based SET delivery over HTTP | The poller, acknowledgements and setErrs |
| RFC 9493 | Subject identifiers for SETs: account, email, iss_sub, opaque, phone_number, did, uri, aliases |
SsfSubjectIdentifier, every format, and SsfSubject for complex subjects; the matching of events to sessions and users |
| RFC 7515 / RFC 7517 | JSON Web Signature and JSON Web Key | Signature checks against the transmitter's JWK Set, with Nimbus JOSE + JWT |