Shared Signals and CAEP
How the OpenID Shared Signals Framework 1.0 and CAEP 1.0 let permdock/ssf invalidate snapshots the moment an identity provider revokes a session instead of waiting for a TTL.
What it is
The OpenID Foundation's Shared Signals Framework (SSF) 1.0 and Continuous Access Evaluation Profile (CAEP) 1.0 were approved as Final Specifications on 2 September 2025 (announcement). SSF is the plumbing: a transmitter (usually an identity provider) sends Security Event Tokens (SETs) to a receiver (an application or another IdP) over either RFC 8935 push (the transmitter POSTs to the receiver) or RFC 8936 poll (the receiver fetches). CAEP is the vocabulary of events about a session's continued validity, including:
session-revoked: the user's session at the IdP ended (sign-out, admin action, risk signal).credential-change: a password, key or MFA factor was changed, added or removed.assurance-level-change: the user's authentication assurance went up or down.
CAEP 1.0 defines eight events in all; the others are session-established, session-presented, token-claims-change, device-compliance-change and risk-level-change. RISC 1.0 is the sibling vocabulary for account-level events such as account-disabled, account-purged and credential-compromise. SSF 1.0 adds a stream verification event that a transmitter sends to check a stream end to end.
Transmitters in production include Microsoft Entra, Okta and Auth0; Keycloak shipped an experimental SSF transmitter in July 2026. PermDock's receiver is tested against SETs in the Entra, Okta and Auth0 shapes; Keycloak joins when its transmitter leaves experimental status (SSF adapter).
Why it matters for PermDock
PermDock's client model is snapshot-based: permdock.snapshot() serialises roles and grants, the React and React Native adapters answer checks from it, and Next.js caches it with "use cache: private" and a tag. A snapshot is only as fresh as its TTL. Without SSF, revoking a user's session at the IdP leaves a stale snapshot granting UI access until the cache expires. With an SSF receiver, the IdP tells PermDock the moment something changes, and PermDock invalidates exactly that user's snapshot and client cache. The snapshot model moves from "stale for N minutes" to "stale until the IdP says so". See snapshots.
The transmitter half exists because PermDock Cloud is an authority too: a role removed in its directory, or a snapshot invalidated by an administrator, has to reach every application that cached it. Shipping the signer, the push client and the poll endpoint in permdock/ssf keeps the Cloud's SETs identical to what the receiver tests accept, instead of a second implementation of the same RFCs.
How PermDock uses it
import { createPermDock } from "permdock/ssf";
import { updateTag } from "next/cache";
import { snapshotTag } from "permdock/next";
export const { receiver } = createPermDock(policy, {
onEvent: {
"session-revoked": ({ subject }) => updateTag(snapshotTag(subject.id)),
"credential-change": ({ subject }) => updateTag(snapshotTag(subject.id)),
"assurance-level-change": ({ subject, event }) => audit(event),
},
});
// Mount `receiver` as the RFC 8935 push endpoint, or run it as an RFC 8936 poller.What the ssf adapter does:
-
Receives SETs over push or poll, verifies the transmitter's signature and issuer according to the stream configuration, and rejects anything that fails verification (fail closed: an unverifiable event is dropped and logged, never acted on).
-
Resolves the subject. CAEP subject identifiers (email, issuer-and-subject, opaque) are mapped to the application's user id through an adapter option, because PermDock does not have a user store.
-
Invalidates. Each handler receives the resolved subject and the raw event. The typical action is
updateTag(snapshotTag(user))in Next.js, or calling the clientinvalidate()channel sousePermDock()refetches. Snapshots scoped withincludeshare the same tag, so one event clears all of a user's scoped snapshots. -
Feeds audit. Events are emitted on the same observability path as decisions (see audit and observability) so a
session-revokedfollowed by adenieddecision is traceable. -
Revokes the account on RISC.
account-disabled,account-purgedandcredential-compromiseend every session of the principal on therevocationsfeed and cancel its pending approvals in theapprovalsstore, because each one means the account itself can no longer be trusted. -
Answers stream verification. A
verificationevent runsonEvent.verificationwith itsstateand is recorded on the audit path; it never touches a snapshot. -
Asks for a retry on its own failures. A handler that throws gets
500back to the transmitter, so RFC 8935 push redelivers;400stays for a SET that can never succeed.
Invalidation is strictly per subject; there is no handler that invalidates everything for an issuer.
permdock/ssf also has the transmitter half, for a service that emits signals: PermDock Cloud, or an application that tells its own downstream services that a session ended.
- Builds events.
caepEvents.sessionRevoked,credentialChange,tokenClaimsChange,assuranceLevelChangeanddeviceComplianceChange(CAEP 1.0), andriscEvents.accountDisabled,accountPurgedandcredentialCompromise(RISC 1.0), each aSetEventwith the event-type URI, thesub_idand snake_case claims. - Signs SETs.
signSet(event, signer, { audience, jti?, txn? })writes an RFC 8417 SET withtyp: secevent+jwtand one event, as SSF 1.0 requires, through anyTokenSigner. - Pushes.
pushSet(set, { endpoint, authorization })is the RFC 8935 transmitter; it returns{ ok: true }or aSetDeliveryFailureCausemapped from the receiver'serr, withretryset for a network failure or a5xx. - Serves polls.
setPollHandler({ queue, authorize })is the RFC 8936 poll endpoint over aSetQueue;memorySetQueue()is the in-process default. - Types the stream.
SsfTransmitterConfiguration(/.well-known/ssf-configuration),SsfStreamConfigurationandSsfStreamStatusfollow SSF 1.0 sections 7 and 8. - Manages streams.
ssfTransmitter({ issuer, streams, queue, signer, authorize })serves the metadata, stream configuration, status and verification endpoints and the per-stream poll endpoint over anSsfStreamStore;memorySsfStreamStore()is the in-process default. - Fans out.
fanOutSet(event, { streams, queue, signer })delivers one event to every stream that requested it, signed once per audience, by push or into the poll queue. - Polls another transmitter.
pollSets({ endpoint, authorization, acks })is one RFC 8936 poll request that answers the SETs unverified.
Adding and removing subjects per stream (SSF 1.0 section 8.1.3) is not supported: ssfTransmitter delivers every subject and says default_subjects: "ALL".
Outside Next.js, the revocations option carries the signal to clients: the receiver publishes session-revoked or changed on a RevocationFeed, and open connections end or revalidate (streams). An app without long-lived connections relies on the client's next revalidation.
Event lifecycle
- The IdP ends a user's session (admin action, risk signal, sign-out everywhere) and emits a
session-revokedSET on the stream the application subscribed to. - The SET reaches
receiver, by POST (RFC 8935) or by the receiver's next poll (RFC 8936). receiververifies the signature and issuer against the stream configuration. On failure the event is logged and dropped.- The subject identifier is resolved to the application's user id through the configured mapper.
- The
onEvent['session-revoked']handler runs:updateTag(snapshotTag(user))in Next.js, or the clientinvalidate()channel elsewhere. - The next request for that user builds a fresh
PermDockand snapshot;usePermDock().statuson connected clients moves tostaleand thenreadyafter refetch. - The event is recorded on the audit path alongside decisions.
Relationship to snapshot TTLs
CAEP does not replace the TTL; it shortens the window. Snapshots still expire so that a missed event (a transmitter outage, a poll gap) cannot leave stale authority indefinitely, and the TTL becomes a backstop rather than the primary freshness mechanism.
Mapping table
| SSF / CAEP concept | PermDock concept |
|---|---|
| Transmitter (Entra, Okta, Auth0, Keycloak) | Configured issuer whose SETs receiver accepts |
| Receiver | receiver from createPermDock in permdock/ssf |
| RFC 8935 push delivery | receiver mounted as an HTTP endpoint |
| RFC 8936 poll delivery | receiver run as a poller with the stream's poll endpoint |
| SET (Security Event Token) | Verified, then dispatched to onEvent by event type |
session-revoked | Invalidate the subject's snapshot and client caches |
credential-change | Invalidate the subject's snapshot (roles may depend on MFA state) |
assurance-level-change | Invalidate only. The next subjectFrom* carries the new acr / amr; assurance() on the grant decides. The event never raises a policy. |
| Other CAEP 1.0 events | Dispatched to onEvent by name; token-claims-change also revalidates open connections |
RISC account-disabled, account-purged, credential-compromise | With revocations and approvals set, revoke every session of the principal and cancel its pending approvals |
SSF verification event | onEvent.verification({ state, jti, issuer }) and a verification audit event |
| Subject identifier formats | Adapter option mapping to the application's principal.id |
| Transmitter role | ssfTransmitter and fanOutSet over an SsfStreamStore and a SetQueue; signSet, pushSet and setPollHandler underneath |
| Unverifiable SET | Dropped and logged; no invalidation, no grant change |
Sources
- OpenID Foundation CAEP and Shared Signals announcements.
- CAEP 1.0, RISC 1.0 and SSF 1.0, including stream verification.
- Keycloak experimental SSF support.
- RFC 8935 (push) and RFC 8936 (poll) are referenced by number.
- Product plan, "Standards and agent runtimes (Sept 2026)" section.
Last updated on
OAuth for agent delegation
How RFC 9396 Rich Authorization Requests, RFC 8693 token exchange, DPoP, CIBA and the IETF agent-delegation drafts shape PermDock's two-principal subject and the rule that an agent can never exceed its user.
WebMCP
How permdock/webmcp registers browser-exposed tools through document.modelContext only when the client snapshot allows them, and how WebMCP hints and Permissions-Policy fit in.