Auditing Logins

Security Events

EventPublished when
AuthenticationSuccessEventa login succeeds (also on every bearer-token request in a resource server)
AbstractAuthenticationFailureEvent subclassesbad credentials, locked, disabled, expired account...
AuthorizationDeniedEventan authorization check fails (needs an AuthorizationEventPublisher bean)
LogoutSuccessEventa 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 -4

Example 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