Skip to main content

Audit log

Every authentication-relevant action and — since 2.2 — every administrative tenant mutation is recorded in the person_auth_log table. The log is a tamper-evident, append-only trail of who did what, when, from which client, and (where applicable) what they changed.

Available since 2.2

target_person_id and event_data columns and most admin-side event types were introduced in engine 2.2. The login/password/IDP events have been recorded since 1.x.

What ends up in the log

Every entry carries:

ColumnMeaning
typeOne of the auth_log_type enum values (see table below)
successWhether the action succeeded; failures are recorded too
created_atTimestamp
person_idThe actor's person (if the actor was a person, not a system token)
identity_idThe actor's identity
target_person_idThe subject of the action when different from the actor (e.g. force sign-out, role grant, membership change)
client_ipThe effective client IP (after trust-forwarded-info is applied)
user_agentThe effective client User-Agent
person_input_identifierFree-form input string (the email submitted on a failed login, etc.)
error_codeThe code returned to the caller on failure
metadataJSONB — forensic context (forwarder IP/UA, sessionId, reason, …)
event_dataJSONB — domain payload: {before, after} snapshots, creation snapshots, redacted inputs

Event types

Authentication events

TypeWhen
loginsignIn (password)
create_session_tokencreateSessionToken (admin impersonation)
idp_loginsignInIDP
passwordless_login_initinitSignInPasswordless
passwordless_login_exchangeactivatePasswordlessOtp
passwordless_loginsignInPasswordless
password_reset_initcreateResetPasswordRequest
password_resetresetPassword
password_changechangePassword, changeMyPassword
email_changechangeProfile, changeMyProfile (email field)
2fa_enable, 2fa_disableconfirmOtp, disableOtp

Session events (since 2.2)

TypeWhentarget_person_idevent_data / metadata
session_revoked_by_userrevokeSessionmetadata.sessionId
forced_sign_outforceSignOutPersontarget personmetadata.reason (if supplied)
person_disabledisablePersontarget person

Authorization changes (since 2.2)

TypeWhenevent_data
global_role_grantaddGlobalIdentityRoles (success){before: {roles}, after: {roles}}
global_role_revokeremoveGlobalIdentityRoles (success){before: {roles}, after: {roles}}
project_membership_createaddProjectMember (success){projectId, identityId, before: [], after: [{role, variables}]}
project_membership_updateupdateProjectMember (success){projectId, identityId, before, after}
project_membership_removeremoveProjectMember (success){projectId, identityId, before, after: []}

target_person_id is resolved from the affected identity, so even when the actor is acting on someone else's identity the trail points at the right person.

Administrative events (since 2.2)

TypeWhenevent_data
api_key_createcreateApiKey, createGlobalApiKey{apiKeyId, identityId, description, memberships?, roles?} — never the token or hash
api_key_disabledisableApiKey{apiKeyId}
idp_createaddIDP{identityProvider, type, configurationKeys, options} — only the key names of the configuration are stored; values (client secrets, OIDC URLs) are never persisted
idp_updateupdateIDP{before, after} with configuration collapsed the same way
idp_disable, idp_enabledisableIDP, enableIDP{identityProvider}
project_createcreateProject{projectSlug, name}
project_updateupdateProject{before, after}
project_secret_changesetProjectSecret{projectSlug, key} — only the secret name, never the value
mail_template_changeaddMailTemplate, removeMailTemplate{action: 'add' | 'remove', type, projectSlug?, variant?}
tenant_config_changeconfigure{input} — full ConfigInput with captcha.secret redacted to ***
person_inviteinvite, unmanagedInvite{projectSlug, email, isNew, memberships}

Reading the log

GraphQL

The tenant API exposes the log via Query.authLog. Access requires the system:viewAuthLog permission — by default only SUPER_ADMIN has it (via the wildcard ALL-resource/ALL-privilege grant). Page size is capped server-side (default 100, max 500); hasMore indicates a further page exists.

query AdminActionsAgainstPerson($target: String!, $after: DateTime!) {
authLog(
limit: 100,
filter: { targetPersonId: $target, createdAfter: $after }
) {
hasMore
entries {
id
createdAt
type
success
invokedByIdentityId
eventData
metadata
}
}
}

In Contember Cloud the same data drives the dashboard view.

AuthLogFilter fields

All fields are AND-combined; omitted fields are unconstrained.

FieldNotes
typesOR-combined list of auth_log_type values.
successFilter to successful or failed events only.
invokedByIdentityIdActor identity.
personIdActor's person.
targetPersonIdSubject of the action — for force sign-out, role grant, membership change, etc.
personInputIdentifierThe free-form identifier submitted by the caller (usually the email on a failed signIn / signUp). Useful for tracking probe attempts against a specific email that does not exist in person.
createdAfterInclusive lower bound (created_at >= createdAfter).
createdBeforeExclusive upper bound (created_at < createdBefore).

Direct SQL

You can also query person_auth_log directly via the tenant database — useful for ad-hoc analytics that don't map cleanly onto the AuthLogFilter input:

SELECT created_at, type, identity_id, success, event_data
FROM person_auth_log
WHERE target_person_id = $1
AND created_at > now() - interval '30 days'
ORDER BY created_at DESC;

Logging conventions

  • Both success and failure are recorded for authentication events. The success flag distinguishes them.
  • Admin events are recorded only on success — a denied permission check throws before the action runs, so there is nothing to audit at the resolver level (the permission failure is observable from the HTTP error response itself).
  • Secret-bearing inputs are redacted before they hit the log: captcha secret becomes ***, IDP configuration is collapsed to a sorted list of keys, project secrets log only their key name, API keys log only their id and identity.
  • The forwarder's socket IP/UA are preserved in metadata.forwarderIp / metadata.forwarderUserAgent when trust-forwarded-info is in effect, so the trail still has a path back to the proxy.