Skip to main content
Version: Next (5.0)

Security audit

AWE records the security events of an application: logins, logouts, account locks, password and second factor changes, SSO provisioning, denied accesses and REST tokens. Every event carries the user, tenant, client address, user agent, a hash of the session and a correlation identifier, and is sent to one or more sinks: a database table, a JSON log line for a SIEM and a Micrometer counter.

The audit is on by default. The only visible change for an existing application is the AweSecAudEvt table, created by the Flyway migration AWE_V1.3.0, and the Security audit screen in the security menu.

You want toRead
Switch the whole feature offTurn it on and off
Know what is recordedEvent model, Event catalogue
Send the events to a SIEM or keep them out of the databaseSinks
Lock accounts after failed loginsAutomatic lockout
Comply with data protection rulesPrivacy
Browse the eventsSecurity audit screen

Turn it on and off​

awe.security.audit.enabled is the global switch. With false no event is recorded by any sink and nothing is purged; the audit service stays available as a no-op, so code that publishes events needs no checks.

# Switch the security audit off
awe.security.audit.enabled=false

Each sink has its own switch as well (see Properties). Products that already audit on their own can, for example, keep the JSON log and drop the table:

awe.security.audit.database.enabled=false

Event model​

An event is a com.almis.awe.model.security.audit.SecurityAuditEvent. The framework fills in the request data when the event is published; values already set on the event are kept.

FieldContent
idUnique identifier (UUID)
timestampMoment of the event
typeEvent kind, see the catalogue
outcomeSUCCESS or FAILURE
causeReason, mainly for failures (may be empty)
usernameUser involved, as typed at the login or the authenticated user (up to 255 characters)
tenantSSO tenant of the request or, without one, the database alias of the session (up to 100 characters)
channelWEB (web application), SSO (identity provider), REST (requests under /api/) or SYSTEM (raised by the server outside any request, such as a session timeout)
ipAddressClient address, anonymized when configured
userAgentUser-Agent header of the client (up to 512 characters)
sessionHashSHA-256 of the session identifier. The session identifier itself is never stored
correlationIdIdentifier shared by the events of one request: the X-Correlation-Id header, else X-Request-Id, else the MDC traceId (tracing), else one generated per request. Header values that are not 1 to 64 characters of letters, digits, ., _, : or - are ignored
detailFree detail: option, URL, profile, administrator, provider error code... (up to 1000 characters)

Event catalogue​

The type and cause values are the enums SecurityAuditEventType and SecurityAuditCause. "Channel" is the channel of the request unless a value is given.

TypeRaised whenOutcome and causeChannel
LOGINA local login (login form, /action/login, POST /api/authenticate) endsSUCCESS; FAILURE with BAD_CREDENTIALS (wrong password or unknown user), LOCKED, DISABLED, ACCOUNT_EXPIRED, CREDENTIALS_EXPIRED, LOCAL_LOGIN_DISABLED or ERRORWEB, REST
LOGINAn SSO login endsSUCCESS; FAILURE with LOCKED, DISABLED... (account refused by AWE), ROLE_NOT_MAPPED, PROVIDER_ERROR (detail: OAuth2 error code) or ERRORSSO
LOGOUTThe user logs outSUCCESS, EXPLICITRequest channel, SSO for SSO sessions
LOGOUTThe identity provider ends the session (OIDC back-channel logout)SUCCESS, PROVIDERSSO
LOGOUTA signed in session times outSUCCESS, EXPIREDSYSTEM
ACCOUNT_LOCKEDThe automatic lockout locks the accountSUCCESS, MAX_ATTEMPTSWEB, REST
ACCOUNT_LOCKEDAn administrator locks the user in the users screenSUCCESS, ADMINISTRATOR (detail: administrator: <user>)WEB
ACCOUNT_UNLOCKEDAn administrator unlocks the user, or resets the password of a locked user, in the users screenSUCCESS, ADMINISTRATORWEB
PASSWORD_CHANGEThe user changes the password in the change password screensSUCCESS; FAILURE, BAD_CREDENTIALS (wrong current password)WEB
PASSWORD_CHANGEThe user replaces an expired password from the loginSUCCESS, CREDENTIALS_EXPIREDWEB
PASSWORD_RESETAn administrator sets a new password in the users screenSUCCESS, ADMINISTRATORWEB
TOTP_ENROLLMENTA second factor secret is generated and storedSUCCESS; FAILURE with FORBIDDEN (a session waiting for its TOTP code tries to rotate the secret) or ERROR (the secret could not be stored)WEB
TOTP_VERIFICATIONThe user types the TOTP code after the loginSUCCESS; FAILURE, INVALID_CODEWEB
TOTP_RESETThe user switches the second factor off, or an administrator resets itSUCCESS (no cause, or ADMINISTRATOR)WEB
SSO_PROVISIONINGA user is created from the identity provider data (awe.security.sso.auto-provision-user)SUCCESS (detail: profile)SSO
SSO_PROFILE_SYNCThe profile of a user is updated from the provider rolesSUCCESS (detail: <old> -> <new>)SSO
ACCESS_DENIEDAn option that exists in the menu is restricted for the user profileFAILURE, RESTRICTED (detail: option: <name>)WEB
ACCESS_DENIEDSpring Security refuses a URL to an authenticated userFAILURE, FORBIDDEN (detail: url: <path>)Request channel
TOKEN_ISSUEDPOST /api/authenticate issues a JWTSUCCESSREST
TOKEN_REJECTEDA REST request carries a malformed, wrongly signed or expired JWTFAILURE, INVALID_TOKEN or TOKEN_EXPIREDREST

Sinks​

The audit service sends every event to every SecurityAuditSink bean. Recording is best effort: a sink that fails is logged and stops neither the other sinks nor the audited operation. Events published inside a transaction (for example a lock made by a maintain) are sent after the commit and dropped if the transaction rolls back, so they only describe changes that really happened.

Database​

DatabaseSecurityAuditSink stores the events in the append-only table AweSecAudEvt, created by the migration AWE_V1.3.0__Security_audit_events.sql for every supported database. The script can be applied again: the table and its indexes are only created when they do not exist. Each event is written on its own connection of the application DataSource and committed at once, outside the transaction of the audited operation. Rows are only deleted by the purge.

ColumnFieldColumnField
IdeSecAudidChnchannel
EvtDattimestampIpAdripAddress
EvtTyptypeUsrAgtuserAgent
EvtResoutcomeSesHshsessionHash
EvtCaucauseCorIdecorrelationId
UsrNamusernameDetdetail
TentenantEvtDayday of timestamp

EvtDay is the day of the event in the server time zone: the date filters and the daily chart of the screen compare plain dates on it, so they behave the same on every database and use its index.

Indexes: EvtDat, (UsrNam, EvtDat), (EvtTyp, EvtDat) and EvtDay.

Retention. The purge deletes the events older than awe.security.audit.database.retention-days (365 by default; 0 keeps them forever) on the awe.security.audit.database.purge-cron schedule (every day at 03:00, server time zone, by default). It runs on its own thread, so the application does not need @EnableScheduling. With several nodes every node runs the same deletion, which is idempotent.

JSON log​

LogSecurityAuditSink writes every event as one JSON line, at INFO level, on the logger awe.security.audit. Empty fields are left out:

{"timestamp":"2026-10-16T08:15:30.123Z","id":"0b5f7c1e-6c1d-4f7e-9a4b-3f2d1c0e9a87","type":"LOGIN","outcome":"FAILURE","cause":"BAD_CREDENTIALS","username":"jdoe","tenant":"awemysql","channel":"WEB","ipAddress":"10.0.12.0","userAgent":"Mozilla/5.0 (X11; Linux x86_64)","sessionHash":"9f2c...e41a","correlationId":"4d1f6a0c-2b7e-4a51-8c0f-7d5e2a9b1c33"}

To ship the events to a SIEM, route that logger to its own file in your logback-spring.xml:

logback-spring.xml
<appender name="SECURITY_AUDIT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH:-logs}/security-audit.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH:-logs}/security-audit.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>

<logger name="awe.security.audit" level="INFO" additivity="false">
<appender-ref ref="SECURITY_AUDIT"/>
</logger>

With the logger above INFO nothing is written.

Micrometer counter​

With the observability starter, MicrometerSecurityAuditSink counts the events on awe.security.audit.events, tagged by type, outcome, channel and cause (none when empty). The user, address and other free values are never tags.

Custom sinks​

Declare a bean that implements com.almis.awe.security.audit.SecurityAuditSink. It receives every event already completed with the request data:

@Bean
public SecurityAuditSink kafkaSecurityAuditSink(KafkaTemplate<String, String> kafka) {
return event -> kafka.send("security-audit", event.getType().name(), event.getId());
}

The built-in sinks are @ConditionalOnMissingBean: a bean of the same class replaces them. To publish events from your own code (for example a custom authentication flow), inject SecurityAuditService and call success, failure or publish.

Automatic lockout​

The automatic lockout is off by default. Set awe.security.lockout.max-attempts to the number of consecutive wrong passwords after which a local account is locked:

awe.security.lockout.max-attempts=5
StepWhat happens
Wrong password of an existing userThe counter ope.NumLog grows by one. Other failures (locked, disabled, expired...) and unknown users are not counted
Counter reaches max-attemptsThe account is locked (ope.PwdLck = 1) and an ACCOUNT_LOCKED event with cause MAX_ATTEMPTS is recorded. Next logins fail with cause LOCKED
Successful loginThe counter goes back to 0
Administrator unlocks the user (or resets the password) in the users screenThe account is unlocked, the counter is cleared and an ACCOUNT_UNLOCKED event is recorded

The lockout only applies to local logins (form, /action/login and /api/authenticate); with SSO, lockout is the identity provider's job. It works even when the audit is switched off. A failure while counting never changes the result of the login.

Privacy​

  • IP anonymization. awe.security.audit.anonymize-ip=true stores the client address with the last octet of IPv4 addresses set to zero (10.0.12.34 → 10.0.12.0) and IPv6 addresses cut after the first 48 bits (2001:db8:85a3:8d3:1319:8a2e:370:7348 → 2001:db8:85a3::). Addresses that cannot be parsed are dropped, never stored in clear.
  • Sessions. Only the SHA-256 hash of the session identifier is stored, so the events can be grouped by session but the identifier cannot be replayed.
  • Behind a proxy or load balancer the address is the one of the proxy unless Spring Boot is told to trust the forwarded headers: set server.forward-headers-strategy=native or framework (see Logging, database and web for war deployments).
  • Retention. Adjust awe.security.audit.database.retention-days to the period your data protection policy allows.

Security audit screen​

Settings > Security access > Security audit (option security-audit, screen SecAud) browses the events stored by the database sink. It is a generic XML screen, rendered by both the Angular and the React clients.

PartContent
CriteriaDate range (the last 7 days by default), event types, outcome, channel, user and IP (partial match)
Activity per dayStacked column chart of successful and failed events
Failures by causePie chart of the failures
EventsPaginated grid, newest first: date, event, outcome, user, cause, channel, IP and detail
ViewDetail of the selected event (screen SecAudViw, option view-security-audit-event), with tenant, identifier, correlation identifier, session hash and user agent

Applications that define their own menu/private.xml (the archetypes do) must add the option to their security group to show the screen:

<option name="security-audit" label="MENU_TOOLS_SECURITY_AUDIT" screen="SecAud" icon="shield">
<option name="view-security-audit-event" screen="SecAudViw" invisible="true"/>
</option>

The screen is a regular menu option: restrict it to the profiles that may see it with the usual profile and user restrictions (see Security mechanisms). Without the database sink the screen shows no events.

Properties​

PropertyDefaultDescription
awe.security.audit.enabledtrueRecord security events. With false no sink records anything and nothing is purged
awe.security.audit.anonymize-ipfalseAnonymize the client address of every event (IPv4 last octet, IPv6 after the first 48 bits)
awe.security.audit.database.enabledtrueStore the events in the AweSecAudEvt table
awe.security.audit.database.retention-days365Days an event is kept before the purge deletes it. 0 keeps the events forever
awe.security.audit.database.purge-cron0 0 3 * * *Spring cron expression (server time zone) of the purge
awe.security.audit.log.enabledtrueWrite every event as one JSON line on the awe.security.audit logger
awe.security.audit.metrics.enabledtrueCount the events on the awe.security.audit.events counter (observability starter)
awe.security.lockout.max-attempts0Consecutive wrong passwords after which a local account is locked. 0 switches the lockout off

They are also listed in the properties reference.

Limitations​

  • No concurrent session control. AWE does not limit the sessions of a user; the audit records the explicit, provider and expiration logouts.
  • Custom authentication flows (filters, controllers or other entry points of your own that authenticate users without the AWE login) are not recorded unless they call SecurityAuditService.
  • The purge runs on every node. The deletion is idempotent, so several nodes only repeat the same work.
  • Multiple databases. With awe.database.multidatabase-enable=true the application DataSource routes by the database of the session: events raised in a request are stored in that database, and the purge, which runs outside any request, cleans the default database.