Skip to content

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.

A SET carries one event and these claims:

  • iss: the realm’s issuer; aud: the receiver’s client_id.
  • jti: the event’s id, and iat: the event’s time, in seconds. A retry sends the same SET, so a receiver drops a duplicate by its jti.
  • 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.

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.

  • A user is iss_sub: the realm’s issuer and the user’s id.
  • A user’s session is a complex subject (SSF 1.0 §3.2): user as above, and session as opaque with the session id, the sid the user’s ID tokens carry. A user.signed_out that 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 uri when its client_id is an absolute URI, as an agent client’s Client ID Metadata Document URL is, and opaque with the client_id otherwise.
  • A key is opaque with its kid.

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.

QAuth event typeSSF eventSubjectDelivered toMembers besides event_timestamp
user.signed_outsession-revoked (CAEP 1.0 §3.1)the user, iss_sub or complexevery stream that added the subjectinitiating_entity from the actor
user.password_resetcredential-change (CAEP 1.0 §3.3)the user, iss_subevery stream that added the subjectcredential_type: "password"; change_type: "update"; initiating_entity from the actor
user.disabledaccount-disabled (RISC 1.0 §2.3)the user, iss_subevery stream that added the subjectinitiating_entity from the actor
user.enabledaccount-enabled (RISC 1.0 §2.4)the user, iss_subevery stream that added the subjectinitiating_entity from the actor
consent.revokedtoken-claims-change (CAEP 1.0 §3.2)the user, iss_subthe stream of the event’s client, when it added the subjectclaims: {"scope":""}; initiating_entity from the actor
token.family_revokedsession-revoked (CAEP 1.0 §3.1)the user, iss_subthe stream of the event’s client, when it added the subjectreason_admin: by data.reason; initiating_entity: "policy"
token.revokedtoken-revoked (QAuth)the user, iss_subthe stream of the event’s client, when it added the subjecttoken_type_hint: data.typeHint; initiating_entity from the actor
client.secret_rotatedcredential-change (CAEP 1.0 §3.3)the client, uri or opaquethe stream of the event’s client, when it added the subjectcredential_type: "client-secret"; change_type: "update"; initiating_entity: "admin"
api_key.createdcredential-change (CAEP 1.0 §3.3)the event’s client, uri or opaquethe stream of the event’s client, when it added the subjectcredential_type: "api-key"; change_type: "create"; initiating_entity: "admin"
api_key.revokedcredential-change (CAEP 1.0 §3.3)the event’s client, uri or opaquethe stream of the event’s client, when it added the subjectcredential_type: "api-key"; change_type: "revoke"; initiating_entity: "admin"
wallet.linkedcredential-change (CAEP 1.0 §3.3)the user, iss_subevery stream that added the subjectcredential_type: "verifiable-credential"; change_type: by data.rebound (true → "update", false → "create"); initiating_entity from the actor
key.emergency_retiredkey-emergency-retired (QAuth)the key, opaque with its kidevery stream of the realm that requested itkid: data.keyId; purpose: data.purpose; initiating_entity from the actor

Notes on the mapping:

  • user.disabled: Sent without reason: RISC 1.0 names only hijacking and bulk-account, and QAuth records no reason for the switch.
  • consent.revoked: A withdrawn consent covered every scope the client held for the user, so the scope of 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 is session-revoked. The replay policy revoked it, so initiating_entity is policy.
  • 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, so initiating_entity is admin.
  • 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, so initiating_entity is admin.
  • 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, so initiating_entity is admin.
  • key.emergency_retired: Defined ahead of emergency retirement itself (#551), which emits it; nothing emits it before then.
QAuth event typeWhy it has no SSF event
user.client_session_endedThe 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_usedCAEP 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_failedNeither 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.elevatedCAEP 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.deletedNo 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.suspendedNo 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_liftedNo 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_changedNo 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_changedNo 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.

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).

  • 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.

MemberRequiredMeaning
event_timestampyesWhen the event happened, in seconds since the epoch (CAEP 1.0 §2).
initiating_entitynoWho invoked it, as CAEP 1.0 §2 names it; see Common members.
token_type_hintnoThe token_type_hint the client sent, such as refresh_token. Absent when it sent none.

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.

MemberRequiredMeaning
event_timestampyesWhen the event happened, in seconds since the epoch (CAEP 1.0 §2).
initiating_entitynoWho invoked it, as CAEP 1.0 §2 names it; see Common members.
kidyesThe kid of the retired key.
purposenoWhat the key was for, such as token_signing.