Careeroza
Auditing Logins
Security Events
| Event | Published when |
|---|---|
AuthenticationSuccessEvent | a login succeeds (also on every bearer-token request in a resource server) |
AbstractAuthenticationFailureEvent subclasses | bad credentials, locked, disabled, expired account... |
AuthorizationDeniedEvent | an authorization check fails (needs an AuthorizationEventPublisher bean) |
LogoutSuccessEvent | a user logs out |
An Audit Listener
@Configuration
public class AuditConfig {
@Bean
AuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher publisher) {
return new SpringAuthorizationEventPublisher(publisher); // enables AuthorizationDeniedEvent
}
}
@Component
public class LoginAuditListener {
private static final Logger log = LoggerFactory.getLogger("SECURITY_AUDIT");
private final AuditEventRepository audit; // e.g. a JPA table: time, user, type, ip, details
public LoginAuditListener(AuditEventRepository audit) { this.audit = audit; }
@EventListener
public void onSuccess(AuthenticationSuccessEvent e) {
if (e.getAuthentication() instanceof JwtAuthenticationToken) return; // skip per-request JWT checks
record("LOGIN_SUCCESS", e.getAuthentication(), null);
}
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent e) {
record("LOGIN_FAILURE", e.getAuthentication(), e.getException().getClass().getSimpleName());
}
@EventListener
public void onDenied(AuthorizationDeniedEvent<?> e) {
record("ACCESS_DENIED", e.getAuthentication().get(), String.valueOf(e.getSource()));
}
private void record(String type, Authentication auth, String reason) {
String ip = auth.getDetails() instanceof WebAuthenticationDetails d ? d.getRemoteAddress() : "-";
String user = LogMasker.email(auth.getName());
log.info("{} user={} ip={} reason={}", type, user, ip, reason);
audit.add(new AuditEvent(auth.getName(), type, Map.of("ip", ip, "reason", String.valueOf(reason))));
}
}Mask Personal Data in Logs
Example
Logs are read by many people and shipped to many tools. Never log passwords, OTPs or full tokens, and mask personal data:
import java.util.regex.*;
public class Main {
static String email(String email) {
int at = email.indexOf('@');
if (at < 0) return "***";
String name = email.substring(0, at);
return name.charAt(0) + "*".repeat(Math.max(1, name.length() - 1)) + email.substring(at);
}
static String phone(String phone) {
return phone.substring(0, 3) + "*".repeat(phone.length() - 7) + phone.substring(phone.length() - 4);
}
static String token(String token) {
return token.length() <= 10 ? "***" : token.substring(0, 10) + "...(" + token.length() + " chars)";
}
static final Pattern SECRET_FIELDS = Pattern.compile("(\"(password|otp|cardNumber)\"\\s*:\\s*\")[^\"]*(\")");
static String scrubJson(String json) {
return SECRET_FIELDS.matcher(json).replaceAll("$1[REDACTED]$3");
}
public static void main(String[] args) {
System.out.println(email("[email protected]"));
System.out.println(phone("+919876543210"));
System.out.println(token("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhc2hhIn0.c2lnbmF0dXJl"));
System.out.println(scrubJson("{\"email\":\"[email protected]\",\"password\":\"S3cret!pass\",\"otp\":\"482913\"}"));
}
}Output
a*********@gmail.com
+91******3210
eyJhbGciOi...(69 chars)
{"email":"[email protected]","password":"[REDACTED]","otp":"[REDACTED]"}Log output
The resulting audit log:
grep SECURITY_AUDIT logs/shopease.log | tail -4Example output
2026-03-01T10:02:11.497+05:30 INFO [shopease] SECURITY_AUDIT : LOGIN_SUCCESS user=a*********@gmail.com ip=49.36.112.18 reason=null 2026-03-01T10:05:40.031+05:30 INFO [shopease] SECURITY_AUDIT : LOGIN_FAILURE user=r***@shopease.in ip=103.21.58.9 reason=BadCredentialsException 2026-03-01T10:05:52.664+05:30 INFO [shopease] SECURITY_AUDIT : LOGIN_FAILURE user=r***@shopease.in ip=103.21.58.9 reason=LockedException 2026-03-01T10:07:03.215+05:30 INFO [shopease] SECURITY_AUDIT : ACCESS_DENIED user=a*********@gmail.com ip=49.36.112.18 reason=GET /api/admin/users