Skip to main content
Version: v0.25.0 (Latest)

Shared Signals Framework (SSF)

The Shared Signals Framework implements the OpenID Shared Signals and Events specification for exchanging security events between services and organizations. When a user's account is compromised, a session is revoked, or credentials change, SSF propagates that signal to all relying parties so they can react immediately, revoking sessions, requiring re-authentication, or triggering step-up verification.

SSF operates on top of the EDK's event system. Internal events with security significance are mapped to standardized SSF event types and transmitted as signed Security Event Tokens (SETs) to external receivers. In the other direction, incoming SETs from external transmitters are validated, mapped to internal event types, and published on the EventHub for local consumers.

Why Shared Signals?

In a federated identity ecosystem, a security event at one party affects all parties that trust that identity. If a user's credentials are compromised at the identity provider, every relying party with an active session for that user needs to know, not minutes later via polling, but immediately.

Without SSF, each relying party independently discovers compromises through its own detection (if it has any), or waits for the user to report the issue. SSF creates a real-time notification channel between identity providers, relying parties, and security services. When the IdP detects a compromise, it sends a SET to all registered receivers, and each receiver can take immediate action.

The EDK's SSF implementation is bidirectional: your application can both transmit signals (when it detects security events) and receive signals (from upstream identity providers or partner organizations).

Supported Event Types

SSF defines event types from two specifications:

CAEP (Continuous Access Evaluation Protocol)

CAEP events signal changes to active sessions and tokens. They enable real-time access revocation without waiting for token expiration.

EventURNMeaning
Session Revokedhttps://schemas.openid.net/.../session-revokedA user's session has been terminated. Relying parties should invalidate their local sessions for this user.
Token Claims Changedhttps://schemas.openid.net/.../token-claims-changeClaims in an active token have changed (e.g., role removed, group membership updated). Relying parties should re-evaluate access decisions.
Credential Changedhttps://schemas.openid.net/.../credential-changeA user's credentials were updated (password change, MFA enrollment). May indicate a security event if not user-initiated.

RISC (Risk Incident Sharing and Coordination)

RISC events signal security incidents that may affect user accounts across providers.

EventURNMeaning
Account Disabledhttps://schemas.openid.net/.../account-disabledAn account has been disabled, typically due to detected compromise or policy violation.
Account Purgedhttps://schemas.openid.net/.../account-purgedAn account has been permanently deleted (GDPR erasure, offboarding). All data for this user should be removed.
Credential Compromisehttps://schemas.openid.net/.../credential-compromiseA user's credentials are believed to be compromised. All sessions should be terminated and re-authentication required.

Security Event Tokens (SET)

SSF events are transmitted as JWTs with type secevent+jwt. Each SET is signed by the transmitter and validated by the receiver. The EDK's SetTokenValidator enforces a strict validation pipeline:

  1. Payload size: reject oversized tokens (default max 64KB)
  2. JWT type: must be secevent+jwt
  3. Algorithm: must be in the allowed set (default RS256, ES256)
  4. Signature: verified against the transmitter's JWKS
  5. Issuer: must be in the configured trusted issuer set
  6. Audience: must match the receiver's configured audience
  7. Freshness: timestamp must be within configured clock skew (default 5 minutes)
  8. Replay detection: JTI (JWT ID) checked against the replay cache to prevent reprocessing

The JtiReplayDetector uses an LRU cache with configurable TTL and maximum size. Entries are tenant-scoped, so JTI uniqueness is enforced per-tenant rather than globally.

Bidirectional Bridge

The SsfEventHubBridge connects the internal event system to external SSF streams in both directions.

Outbound (Internal → External)

When a security-relevant event is published on the EventHub, the bridge:

  1. Subscribes to events in subsystem SHARED_SIGNALS with category SECURITY
  2. Maps internal event types to SSF URNs (e.g., CAEP_SESSION_REVOKEDhttps://schemas.openid.net/.../session-revoked)
  3. Creates a SharedSignalEvent with issuer, audience, timestamp, unique JTI, and event payload
  4. Passes the event to the configured SsfTransmitter for delivery

Your application publishes internal events normally. The bridge handles the mapping and transmission, application code doesn't need to know about SSF.

Inbound (External → Internal)

When a SET is received from an external transmitter:

  1. The SetTokenValidator validates the JWT (signature, issuer, audience, replay, freshness)
  2. The SsfEventHubBridge maps SSF URNs back to internal event types
  3. An internal Event is created with the SSF payload (issuer, JTI, timestamp, and all event-specific claims)
  4. The event is published on the EventHub

Downstream consumers (session managers, cache invalidators, audit loggers) subscribe to these events through the normal event system. They don't need to know the event originated from an external SSF stream.

Delivery Modes

SSF supports two delivery modes:

Push: external transmitters POST signed SETs to your application's push endpoint. The EDK validates the SET and publishes it to the EventHub. This is the preferred mode for real-time security signals.

Poll: the EDK periodically fetches SETs from a transmitter's poll endpoint. The poll interval is configurable (default 60 seconds). This mode is useful when the transmitter doesn't support push or when network topology prevents inbound connections.

Configuration

sphereon:
ssf:
enabled: true

# Trust configuration
trusted-issuers:
- "https://idp.example.com"
- "https://partner.example.com"
audience: "https://api.myapp.com"
allowed-algorithms:
- RS256
- ES256

# Signing (for outbound SETs)
signing-key-ref: "ssf-signing-key"

# Delivery
push-endpoint: "/ssf/events"
poll-endpoint: null
poll-interval-seconds: 60

# Replay detection
jti-cache-ttl-seconds: 86400 # 24 hours
jti-cache-max-size: 100000

# Safety
clock-skew-seconds: 300 # 5 minutes
max-payload-size-bytes: 65536 # 64KB

# Event type filtering (empty = accept all)
accepted-event-types: []

The trusted-issuers list is critical, only SETs from these issuers will be accepted. The signing-key-ref points to a KMS key used to sign outbound SETs.

Integration Example

Handling an inbound credential compromise signal to invalidate all sessions for the affected user:

// Subscribe to RISC credential compromise events
eventHub.subscribe(edkEventFilter {
type(SsfEventTypes.RISC_CREDENTIAL_COMPROMISE)
}).collect { event ->
val subjectId = event.payload["sub"] as? String ?: return@collect
val issuer = event.payload["ssf_iss"] as? String

logger.warn("Credential compromise for $subjectId from issuer $issuer")

// Revoke all sessions for this user
sessionService.revokeAllSessions(subjectId)

// Invalidate cached authorization decisions
authZenCache.invalidateBySubject(subjectId)

// Record in audit log
auditLogService.record(AuditEvent(
commandId = "ssf.credential-compromise",
command = "credential-compromise",
subjectId = subjectId,
result = AuditEventResult.STARTED,
timestamp = Clock.System.now().toEpochMilliseconds()
))
}

Modules

ModuleDescription
lib-ssf-publicSharedSignalEvent, SsfEventTypes, SetTokenValidator, SsfTransmitter, SsfReceiver, JtiReplayDetector, configuration
lib-ssf-implKache-backed replay detector, SsfEventHubBridge, SET validation pipeline