SSF events
QAuth’s SSF transmitter sends the events that record a security change to receivers’ streams, as Security Event Tokens (SETs, RFC 8417). Each is sent as a CAEP 1.0 or RISC 1.0 event where one says the same thing, and as a QAuth-defined event where none does. Resource servers and relying parties use them to stop honouring a revoked session, grant or credential before its tokens expire (ADR-019 Decision 8).
The event registry marks every type that records a security change: a session, a grant, a token, a credential, a key or an account that a receiver may have to act on. Each security type declares the SSF event it is sent as, or that it has none; the registry’s test fails when one declares neither, so no feature adds a security event and forgets SSF. This page is generated from those declarations.
The SET
Section titled “The SET”A SET carries one event and these claims:
iss: the realm’s issuer;aud: the receiver’sclient_id.jti: the event’s id, andiat: the event’s time, in seconds. A retry sends the same SET, so a receiver drops a duplicate by itsjti.sub_id: the subject (SSF 1.0 §3, RFC 9493), as below.events: one member, keyed by the SSF event’s URI.
It carries no sub and no exp (SSF 1.0 §4.1.2, §4.1.7). Its header names the signing key
(kid) and typ: secevent+jwt; the keys are at the transmitter’s jwks_uri.
Common members
Section titled “Common members”Every mapped event carries event_timestamp, the time the event happened in seconds (CAEP
1.0 §2). It carries initiating_entity when QAuth knows who invoked it: user for a person acting on their own account, admin for an administrator (a person acting on an admin event, or an operator on the server host), and system for QAuth itself. A client acting for itself names none.
A mapping may fix it instead, as the replay policy does.
Subjects
Section titled “Subjects”- A user is
iss_sub: the realm’s issuer and the user’s id. - A user’s session is a
complexsubject (SSF 1.0 §3.2):useras above, andsessionasopaquewith the session id, thesidthe user’s ID tokens carry. Auser.signed_outthat ended one session names it so, and a receiver ends that session alone; one that ended every session of the user names the user alone. A stream that added the user receives both. - A client is
uriwhen itsclient_idis an absolute URI, as an agent client’s Client ID Metadata Document URL is, andopaquewith theclient_idotherwise. - A key is
opaquewith itskid.
The transmitter’s default_subjects is NONE: a stream receives an event only when it
requested the event and its receiver added the subject, except an event about a key, which every
stream of the realm that requested it receives. An event marked as going to the event’s client
reaches only the stream whose receiver is that client: it tells of the user’s dealings with that
one client, which another receiver has no need to see.
A receiver may add a user only when the user is related to it: the user has an active consent for
the receiver’s client or a live refresh token it was issued, or, for a receiver that serves as a
resource server, a live refresh token whose audience names it. Any other user, and a user id that
does not exist, is refused with 403, as SSF 1.0 §8.1.3.2 prescribes for a subject the receiver
may not add. A user subject lasts as long as its relation: the hourly ssf-subject-sweep job
deletes it once the relation has ended and every event about the user has reached the stream, so
the SET that tells of the end still arrives. A disabled user is related to no receiver, so after
account-disabled reaches a stream the sweep deletes the user’s subject there, and
account-enabled reaches only a stream the sweep has not cleared yet: once the user is enabled,
the receiver adds them again. The SSF transmitter guide gives the
whole rule.
Mapping
Section titled “Mapping”| QAuth event type | SSF event | Subject | Delivered to | Members besides event_timestamp |
|---|---|---|---|---|
user.signed_out | session-revoked (CAEP 1.0 §3.1) | the user, iss_sub or complex | every stream that added the subject | initiating_entity from the actor |
user.password_reset | credential-change (CAEP 1.0 §3.3) | the user, iss_sub | every stream that added the subject | credential_type: "password"; change_type: "update"; initiating_entity from the actor |
user.disabled | account-disabled (RISC 1.0 §2.3) | the user, iss_sub | every stream that added the subject | initiating_entity from the actor |
user.enabled | account-enabled (RISC 1.0 §2.4) | the user, iss_sub | every stream that added the subject | initiating_entity from the actor |
consent.revoked | token-claims-change (CAEP 1.0 §3.2) | the user, iss_sub | the stream of the event’s client, when it added the subject | claims: {"scope":""}; initiating_entity from the actor |
token.family_revoked | session-revoked (CAEP 1.0 §3.1) | the user, iss_sub | the stream of the event’s client, when it added the subject | reason_admin: by data.reason; initiating_entity: "policy" |
token.revoked | token-revoked (QAuth) | the user, iss_sub | the stream of the event’s client, when it added the subject | token_type_hint: data.typeHint; initiating_entity from the actor |
client.secret_rotated | credential-change (CAEP 1.0 §3.3) | the client, uri or opaque | the stream of the event’s client, when it added the subject | credential_type: "client-secret"; change_type: "update"; initiating_entity: "admin" |
api_key.created | credential-change (CAEP 1.0 §3.3) | the event’s client, uri or opaque | the stream of the event’s client, when it added the subject | credential_type: "api-key"; change_type: "create"; initiating_entity: "admin" |
api_key.revoked | credential-change (CAEP 1.0 §3.3) | the event’s client, uri or opaque | the stream of the event’s client, when it added the subject | credential_type: "api-key"; change_type: "revoke"; initiating_entity: "admin" |
wallet.linked | credential-change (CAEP 1.0 §3.3) | the user, iss_sub | every stream that added the subject | credential_type: "verifiable-credential"; change_type: by data.rebound (true → "update", false → "create"); initiating_entity from the actor |
key.emergency_retired | key-emergency-retired (QAuth) | the key, opaque with its kid | every stream of the realm that requested it | kid: data.keyId; purpose: data.purpose; initiating_entity from the actor |
Notes on the mapping:
user.disabled: Sent withoutreason: RISC 1.0 names onlyhijackingandbulk-account, and QAuth records no reason for the switch.consent.revoked: A withdrawn consent covered every scope the client held for the user, so thescopeof its tokens for that user is now empty, and QAuth revoked the refresh tokens the consent covered.token.family_revoked: A refresh family is the client’s standing session for the user, so its revocation issession-revoked. The replay policy revoked it, soinitiating_entityispolicy.token.revoked: Sent only when the revoked token was a user’s: the event’s subject. A revocation that matched no token of the client, or a refresh token whose family was revoked already, records the event without one, and no stream receives it.client.secret_rotated: Sent only to the stream of the client whose credential changed, never to another receiver that added that client: a receiver learns of its own credentials only. A developer managing a client acts as its administrator, soinitiating_entityisadmin.api_key.created: Sent only to the stream of the client whose credential changed, never to another receiver that added that client: a receiver learns of its own credentials only. A developer managing a client acts as its administrator, soinitiating_entityisadmin.api_key.revoked: Sent only to the stream of the client whose credential changed, never to another receiver that added that client: a receiver learns of its own credentials only. A developer managing a client acts as its administrator, soinitiating_entityisadmin.key.emergency_retired: Defined ahead of emergency retirement itself (#551), which emits it; nothing emits it before then.
Security events with no SSF event
Section titled “Security events with no SSF event”| QAuth event type | Why it has no SSF event |
|---|---|
user.client_session_ended | The end of a session is user.signed_out, sent as CAEP 1.0 session-revoked. This event is the per-client notice of OpenID Connect Back-Channel Logout 1.0, delivered as a logout token to the client, not as a SET. |
user.recovery_code_used | CAEP 1.0 has no event for a sign-in that used a fallback authenticator, and credential-change is for a credential created, revoked, updated or deleted: a used code is spent, not changed. |
user.recovery_codes_failed | Neither CAEP 1.0 nor RISC 1.0 has an event for failed attempts at an account’s credential: the account is not disabled and no credential changed. |
step_up.elevated | CAEP 1.0 §3.4 assurance-level-change signals that the authentication method changed, and needs the level before and after in a namespace. QAuth’s step-up asks for a fresh sign-in with the same method and records no assurance level, so that event would say nothing true. It maps once step-up can raise the level, with passkeys or TOTP (ADR-018 §3). |
client.deleted | No CAEP 1.0 or RISC 1.0 event describes a client’s suspension or deletion: RISC account-disabled is about an account a person signs in to. |
client.suspended | No CAEP 1.0 or RISC 1.0 event describes a client’s suspension or deletion: RISC account-disabled is about an account a person signs in to. |
client.suspension_lifted | No CAEP 1.0 or RISC 1.0 event describes a client’s suspension or deletion: RISC account-disabled is about an account a person signs in to. |
client.first_party_changed | No CAEP 1.0 or RISC 1.0 event describes a change of trust in a client. The tokens the removal revoked were the client’s, not a person’s session. |
client.verified_domain_changed | No CAEP 1.0 or RISC 1.0 event describes a change of trust in a client: it changes what screens show about the client, not a person’s session or credentials. |
QAuth-defined events
Section titled “QAuth-defined events”Where no CAEP 1.0 or RISC 1.0 event says what happened, QAuth defines one. Its identifier is
urn:qauth:secevent:<name>:v<N>, a URN rather than a URL so that it never looks like a standard
event, and versioned so that a standard one can later sit beside it (ADR-018 §5). Until QAuth 1.0.0
every QAuth-defined identifier is a release candidate, :v<N>-rc.<M>, and nothing about it is
promised: a breaking change raises <M>. The release that ships 1.0.0 drops the suffix. A
receiver ignores members it does not know (SSF 1.0 §4.2.3).
token-revoked
Section titled “token-revoked”- Identifier:
urn:qauth:secevent:token-revoked:v1-rc.0 - Sent for:
token.revoked
A client revoked a token of the subject at the RFC 7009 revocation endpoint. The SET goes to the stream of that client only. QAuth defines it because no CAEP event matches: session-revoked would say the whole session ended, and credential-change is about a credential the subject authenticates with.
| Member | Required | Meaning |
|---|---|---|
event_timestamp | yes | When the event happened, in seconds since the epoch (CAEP 1.0 §2). |
initiating_entity | no | Who invoked it, as CAEP 1.0 §2 names it; see Common members. |
token_type_hint | no | The token_type_hint the client sent, such as refresh_token. Absent when it sent none. |
key-emergency-retired
Section titled “key-emergency-retired”- Identifier:
urn:qauth:secevent:key-emergency-retired:v1-rc.0 - Sent for:
key.emergency_retired
An operator retired a key of the realm that may be compromised (ADR-019 Decision 9). It left the JWKS at once and no verifier in the auth server accepts it. A verifier drops the key the SET names. The subject is the key, as an opaque identifier holding its kid, and every stream of the realm that requested the event receives it.
| Member | Required | Meaning |
|---|---|---|
event_timestamp | yes | When the event happened, in seconds since the epoch (CAEP 1.0 §2). |
initiating_entity | no | Who invoked it, as CAEP 1.0 §2 names it; see Common members. |
kid | yes | The kid of the retired key. |
purpose | no | What the key was for, such as token_signing. |