Skip to main content
Shared signals let identity systems tell each other that something security relevant happened: a session was revoked, an account was disabled, a credential changed. theAuth implements the OpenID Shared Signals Framework 1.0 with the CAEP and RISC event types. One plugin, sharedSignals(), can run either or both roles:
  • Transmitter. Receivers create a stream, choose events and a delivery method, and theAuth sends them signed Security Event Tokens (SETs, RFC 8417).
  • Receiver. theAuth verifies SETs from a transmitter you trust and runs handlers. By default it revokes local sessions when the transmitter says a session, all sessions, or an account is gone.
Nothing is emitted from your existing sign-in paths unless you wire it in (see Emit from existing paths).
A theAuth transmitter sends signed security events to two receivers

One transmitter, many receivers. Each stream picks its events and delivery.

Why you would use it

Without shared signals, a disabled account in one system stays signed in everywhere else until each session expires. With them, the system that knows tells the others in seconds.

Revoke everywhere

Ban a user once and every connected app drops their sessions.

React to risk

Credential compromise or a device falling out of compliance can force re-authentication.

Standard, not custom

Interoperates with any SSF receiver or transmitter, not only theAuth.

Safe by default

Signed events, replay protection, SSRF-safe delivery, no events until you opt in.

How it works

Four steps: something happens, theAuth signs an event, it is delivered, the receiver acts

From an admin action to a revoked session on the other side.

Try it in two steps

1

Run a transmitter

Add sharedSignals({ transmitter }) and wire bridge.onAdminAction (see Install and Emit from existing paths). Banning a user now emits account-disabled and sessions-revoked.
2

Run a receiver

In the other app add sharedSignals({ receiver: { issuer, audience } }) and register a stream with the transmitter pointing at POST /ssf/receive. Sessions for the banned user are revoked there with no extra code.

Install

The plugin id is theauth-shared-signals. When it is present theAuth creates three tables: theauth_ssf_streams, theauth_ssf_subjects and theauth_ssf_outbox. Publish the public key at jwksUri yourself. If you use the OIDC provider with the same key, its JWKS endpoint already does.

Transmitter

Endpoints

Paths are relative to your mount path. endpointBaseUrl must be the absolute URL these are served at, because it is advertised in discovery. Every management and poll endpoint calls your authenticate function. A receiver only sees its own streams. Each endpoint declares a rate limit in its metadata.

Streams

events_delivered is the intersection of events_requested and the events the transmitter supports. Delivery is urn:ietf:rfc:8935 (push) or urn:ietf:rfc:8936 (poll). For poll streams the response carries the poll URL in delivery.endpoint_url. authorization_header is stored as given and is never returned.

Subjects

Subject identifiers follow RFC 9493: iss_sub, email, opaque and complex (with user, device, session, application, tenant, org_unit and group members). defaultSubjects sets the mode of new streams. With ALL (the default) every subject is delivered and subjects/remove excludes one. With NONE nothing is delivered until subjects/add includes a subject. A stream that follows a user also receives events whose complex subject has that user as its user member.

Emitting events

emit signs one SET per matching stream, stores it, and delivers it. Builders exist for every supported event:

Push delivery

Push: the transmitter posts to the receiver. Poll: the receiver asks and acknowledges.

Pick push for latency, poll when the receiver cannot expose a public URL.

SETs are POSTed as application/secevent+jwt with the stream’s Authorization header. A 2xx marks the SET delivered. 408, 429, 5xx and network errors are retried with exponential backoff (default 1 s doubling to 5 min, up to 5 attempts). Other statuses, including an RFC 8935 400 error, and redirects fail the SET without retry. Push URLs go through the same checks as other outbound fetches in theAuth: https only, no credentials, default port, no localhost or metadata hostnames, and every resolved address must be public. The check runs when the stream is configured and again before each attempt. Redirects are never followed. delivery options: Retries in background mode run on in-process timers. If the process exits, pending SETs stay in theauth_ssf_outbox; call signals.transmitter.processDue() from a scheduled job to resume them. Serverless runtimes that freeze after the response should use inline or run processDue() on a schedule.

Poll delivery

POST /ssf/poll/:stream_id accepts maxEvents, returnImmediately, ack and setErrs and answers { "sets": { "<jti>": "<jwt>" }, "moreAvailable": false }. Unacknowledged SETs are returned again. returnImmediately: false waits up to longPollMs (default 0, so it answers at once).

Receiver

The push endpoint is POST /ssf/receive (configurable with path). Keys come from, in order: verificationKey, a static jwks, or discovery. Discovery fetches ${issuer}/.well-known/ssf-configuration (or configurationUrl), requires its issuer to equal the configured issuer, then fetches jwks_uri, all through the SSRF-safe fetcher. An unknown kid triggers at most one refetch every 30 seconds. Checks on every SET: signature, typ of secevent+jwt, iss, aud, an events object, no exp, an iat no older than maxAgeSeconds (default 3600), and a jti not seen before. Replays are acknowledged with 202 and not dispatched again. Failures answer 400 with an RFC 8935 err such as invalid_key, invalid_issuer or invalid_audience. The replay cache is per process memory by default. Pass replayStorage (any SecondaryStorage) when you run several instances.

Default session revocation

When the app has a session manager, these events revoke local sessions unless you set revokeSessions: false: User mapping: an iss_sub from the configured issuer maps sub to the local user id, an email is looked up in the user table, and an opaque id is taken as the user id. Pass resolveUserId to replace this. Handlers you supply take precedence over the defaults for the same event type.

Emit from existing paths

The bridge turns existing callbacks into events. It is opt in: nothing is emitted until you pass the callback.
The callbacks are no-ops until the plugin has initialized, and they swallow errors so a receiver outage never blocks a sign-out or password change. The default subject is iss_sub with the transmitter issuer and the user id; pass subjectForUser to sharedSignals to change it.

Limits

  • Subject formats beyond iss_sub, email, opaque and complex (account, phone_number, did, uri, and so on) are rejected.
  • Per-subject stream status is not supported (501). Subject verified is stored but not enforced.
  • The stream-updated event is not sent.
  • Streams are authorized by your authenticate function only. There is no built-in OAuth scope model for management calls.
  • Delivery is at least once. Receivers should treat jti as an idempotency key.
  • The platform fetch resolves the push hostname again when connecting, so a DNS rebinding attacker with a very short TTL can race the address check. Pass a fetchImpl that pins the validated address to close this.
  • Database support follows the rest of the SDK: tables use the shared SQLite-style schema definitions.
Last modified on October 13, 2026