Security Events¶
SystemForge records security-relevant actions as structured events through
contract/audit: every event is an audit.Event handed to an
audit.Emitter. The events answer the first questions of an incident: who
signed in or was refused, which client was refused a token and why, whether
a token was replayed, what was revoked, who changed whose role, and what the
server was running with.
No event carries a token, an authorization code, a PKCE verifier, a client secret, a key, a DPoP proof or a session cookie. A test runs the sign-in, token and sign-out flows with logging at debug level and fails if any of them appears in a log record or an event.
Recording events¶
| Component | Option |
|---|---|
identity/systemauth |
systemauth.WithAuditEmitter(e) |
identity/systemauthsvc |
Options.AuditEmitter; it is passed to SystemAuth and to the services the package builds |
identity/organization |
organization.WithAuditEmitter(e) |
identity/principal |
principal.WithAuditEmitter(e) |
identity/serviceaccount |
serviceaccount.Config.Audit |
Without an emitter, events are discarded. audit.NewLogEmitter(logger)
writes each event as one log record with the message security event, at
warning level when its outcome is failure and info level otherwise. The
systemauth command uses it, so its log carries the security events.
An application with an audit store wraps the store's Record in an
Emitter.
ocsfaudit.New(w, opts) (package contract/audit/ocsfaudit) writes each
event as one line of OCSF JSON for a security data lake; see
OCSF Security Events. audit.NewMultiEmitter hands
every event to several emitters, such as both of these.
Common fields¶
| Field | Content |
|---|---|
event_type |
The event type listed below |
action |
What was done |
outcome |
success or failure |
actor |
Who acted: a principal (id), an OAuth client (type: application, identifier: client ID), or SystemAuth itself (type: service) |
resource |
What the action applied to |
context.request_id |
The ID of the HTTP request (chi's middleware.RequestID) |
context.client_ip |
The client address (SystemAuth's events) |
context.user_agent |
The client's User-Agent (SystemAuth's events), left out when it holds an email address or a credential-like value (audit.SanitizeUserAgent) |
context.tenant_id |
The organization (membership events) |
details |
Event-specific fields below |
The client address comes from SystemAuth's client-address rule
(client_address, see Client Addresses and Rate Limiting),
the same one rate limits use, so behind a trusted proxy it is the visitor's
address. For a refused token request the client_id is the one the request
names; the client may not have been authenticated.
SystemAuth events¶
| Event type | Action | Outcome | Details |
|---|---|---|---|
systemauth.sign_in |
sign_in |
success |
method (social provider, or login_token), client_id (login token) |
systemauth.sign_in |
sign_in |
failure |
method, client_id, reason |
systemauth.sign_out |
sign_out |
success |
method, tokens_revoked |
systemauth.token.refused |
issue_token |
failure |
grant_type, client_id, reason, error (RFC 6749 error code) |
systemauth.refresh_token.reuse |
redeem_refresh_token |
failure |
grant_type, client_id, family_revoked |
systemauth.authorization_code.reuse |
redeem_authorization_code |
failure |
grant_type, client_id, family_revoked |
systemauth.token.revoked |
revoke |
success or failure |
client_id, token_type_hint, error |
systemauth.token_family.revoked |
revoke |
success |
reason, subject, client_id; resource.identifier is the family's request ID |
systemauth.email_verification.sent |
send_verification |
success |
purpose (verify_email or email_change), sent (false when a send limit suppressed the message or delivery failed); the address is never recorded |
systemauth.email.verified |
verify_email |
success |
purpose, method (the pending sign-in's provider, for verify_email) |
systemauth.email_change.requested |
request_email_change |
success |
none; resource.id is the principal |
systemauth.email_change.cancelled |
cancel_email_change |
success |
none; resource.id is the principal |
systemauth.email.changed |
change_email |
success |
none; resource.id is the principal. Neither address is recorded |
systemauth.signing_key.loaded |
load |
success |
key_id, verification_key_ids, ephemeral |
systemauth.production.waiver |
waive |
success |
check, setting, risk (from systemauthsvc, at startup) |
A signing-key rotation shows at the next start as a new key_id, with the
previous key among the verification_key_ids.
Sign-in refusal reasons¶
| Reason | Meaning |
|---|---|
upstream_denied |
The social provider returned an error |
invalid_state |
The login state was missing, did not match the browser, was used or expired |
missing_code |
The provider's callback had no code |
upstream_exchange_failed, upstream_profile_failed |
The provider's token or profile request failed |
email_not_verified, email_conflict |
The account's email is not verified, or belongs to another account |
principal_inactive |
The account is disabled |
not_invited |
Sign-up is by invitation and there was none |
verification_invalid |
An email verification link was invalid, expired, replaced, already used or opened without the browser that asked for it |
login_token_rejected |
A managed principal's login token was refused |
server_error |
SystemAuth failed |
Token refusal reasons¶
| Reason | Meaning |
|---|---|
unreadable_request |
The body could not be read |
dpop_proof |
The DPoP proof was refused, or a code bound to a key had none |
request_invalid |
Fosite refused the request: client authentication, grant, code, refresh token, PKCE or scope |
production_policy |
A grant or client the production policy refuses |
dpop_binding |
The tokens could not be bound to the proof's key, or a bound grant had no proof by its key |
principal_inactive |
The principal is no longer active; its family is revoked |
managed_principal_refused |
The client does not allow managed principals |
token_family_lifetime |
The refresh-token family outlived its absolute lifetime; it is revoked |
response_failed |
Issuing the tokens failed, or a concurrent exchange used the code |
server_error |
SystemAuth failed |
Token family revocation reasons¶
principal_inactive and token_family_lifetime, as above. Fosite revokes
the family itself on refresh-token reuse and code replay; those are recorded
as their own events with family_revoked: true.
Identity service events¶
| Event type | Actions | Resource | Details |
|---|---|---|---|
organization.membership |
added, updated, removed, ownership_transferred |
membership, the principal's ID |
organization_id, principal_id, role; old_role, was_active, active for updated; previous_owner_id, previous_owner_role for ownership_transferred |
principal.platform_admin |
granted, revoked |
principal |
none |
service_account |
created, disabled, enabled, deleted, key_added, key_revoked, role_granted, role_removed |
service_account |
identifier, organization_id; key_id, algorithm, expires_at, reason for keys; application, role for roles |
The actor of membership and platform-administration events is the
request's principal (requestprincipal) when the change is made in an
authenticated request; the actor of service-account events is the operator
the call names.
Error logging¶
SystemAuth logs a refused OAuth request with an error group holding the
RFC 6749 code, description, hint and status, never the error value
itself, so neither the text nor the JSON format prints a stack trace. The
wrapped cause and Fosite's debug text are added as error.cause and
error.debug when the logger is enabled at debug level, and the cause for
server errors at any level.