Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this short article, we’ll explore the Spring Boot Actuator module and the support for publishing authentication and authorization events in conjunction with Spring Security.

2. Maven Dependencies

First, we need to add the spring-boot-starter-actuator to our pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>3.1.5</version>
</dependency>

The latest version is available in the Maven Central repository.

3. Listening for Authentication and Authorization Events

To log all authentication and authorization attempts in a Spring Boot application, we can just define a bean with a listener method:

@Component
public class LoginAttemptsLogger {

    @EventListener
    public void auditEventHappened(
      AuditApplicationEvent auditApplicationEvent) {
        AuditEvent auditEvent = auditApplicationEvent.getAuditEvent();
        System.out.println("Principal " + auditEvent.getPrincipal() 
          + " - " + auditEvent.getType());
        WebAuthenticationDetails details = 
          (WebAuthenticationDetails) auditEvent.getData().get("details");
        System.out.println("Remote IP address: " + details.getRemoteAddress());
        System.out.println("Session Id: " + details.getSessionId());
    }
}

Note that we’re just outputting some of the things that are available in AuditApplicationEvent to show what information is available. In an actual application, you might want to store that information in a repository or cache to process it further.

Note that any Spring bean will work; the basics of the new Spring event support are quite simple:

  • annotate the method with @EventListener
  • add the AuditApplicationEvent as the sole argument of the method

The output of running the application will look something like to this:

Principal anonymousUser - AUTHORIZATION_FAILURE
  Remote IP address: 0:0:0:0:0:0:0:1
  Session Id: null
Principal user - AUTHENTICATION_FAILURE
  Remote IP address: 0:0:0:0:0:0:0:1
  Session Id: BD41692232875A5A65C5E35E63D784F6
Principal user - AUTHENTICATION_SUCCESS
  Remote IP address: 0:0:0:0:0:0:0:1
  Session Id: BD41692232875A5A65C5E35E63D784F6

In this example, three AuditApplicationEvents have been received by the listener:

  1. Without logging on, access has been requested to a restricted page
  2. A wrong password has been used while logging on
  3. A correct password has been used the second time around

4. An Authentication Audit Listener

If the information exposed by Spring Boot’s AuthorizationAuditListener is not enough, you can create your own bean to expose more information.

Let’s have a look at an example, where we also expose the request URL that was accessed when the authorization fails:

@Component 
public class ExposeAttemptedPathAuthorizationAuditListener extends AbstractAuthorizationAuditListener {

    public static final String AUTHORIZATION_FAILURE = "AUTHORIZATION_FAILURE";

    @Override
    public void onApplicationEvent(AuthorizationEvent event) {
        if (event instanceof AuthorizationDeniedEvent) {
            onAuthorizationFailureEvent(event);
        }
    }

    private void onAuthorizationFailureEvent(AuthorizationEvent event) {
        String name = this.getName(event.getAuthentication());
        Map<String, Object> data = new LinkedHashMap<>();
        Object details = this.getDetails(event.getAuthentication());
        if (details != null) {
            data.put("details", details);
        }
        publish(new AuditEvent(name, "AUTHORIZATION_FAILURE", data));
    }

    private String getName(Supplier authentication) {
        try {
            return authentication.get().getName();
        } catch (Exception exception) {
            return "";
        }
    }

    private Object getDetails(Supplier authentication) {
        try {
            return (authentication.get()).getDetails();
        } catch (Exception exception) {
            return null;
        }
    }
}

We can now log the request URL in our listener:

@Component
public class LoginAttemptsLogger {

    @EventListener
    public void auditEventHappened(
      AuditApplicationEvent auditApplicationEvent) {
        AuditEvent auditEvent = auditApplicationEvent.getAuditEvent();
        System.out.println("Principal " + auditEvent.getPrincipal() 
          + " - " + auditEvent.getType());

        WebAuthenticationDetails details
          = (WebAuthenticationDetails) auditEvent.getData().get("details");
        System.out.println("Remote IP address: " + details.getRemoteAddress());
        System.out.println("Session Id: " + details.getSessionId());
        System.out.println("Request URL: " + auditEvent.getData().get("requestUrl"));
    }
}

As a result, the output now contains the requested URL:

Principal anonymousUser - AUTHORIZATION_FAILURE
  Remote IP address: 0:0:0:0:0:0:0:1
  Session Id: null
  Request URL: /hello

Note that we extended from the abstract AbstractAuthorizationAuditListener in this example, so we can use the publish method from that base class in our implementation.

If you want to test it, check out the source code and run:

mvn clean spring-boot:run

Thereafter you can point your browser to http://localhost:8080/.

5. Storing Audit Events

By default, Spring Boot stores the audit events in an AuditEventRepository. If you don’t create a bean with an own implementation, then an InMemoryAuditEventRepository will be wired for you.

The InMemoryAuditEventRepository is a kind of circular buffer that stores the last 4000 audit events in memory. Those events can then be accessed via the management endpoint http://localhost:8080/auditevents.

This returns a JSON representation of the audit events:

{
  "events": [
    {
      "timestamp": "2017-03-09T19:21:59+0000",
      "principal": "anonymousUser",
      "type": "AUTHORIZATION_FAILURE",
      "data": {
        "requestUrl": "/auditevents",
        "details": {
          "remoteAddress": "0:0:0:0:0:0:0:1",
          "sessionId": null
        },
        "type": "org.springframework.security.access.AccessDeniedException",
        "message": "Access is denied"
      }
    },
    {
      "timestamp": "2017-03-09T19:22:00+0000",
      "principal": "anonymousUser",
      "type": "AUTHORIZATION_FAILURE",
      "data": {
        "requestUrl": "/favicon.ico",
        "details": {
          "remoteAddress": "0:0:0:0:0:0:0:1",
          "sessionId": "18FA15865F80760521BBB736D3036901"
        },
        "type": "org.springframework.security.access.AccessDeniedException",
        "message": "Access is denied"
      }
    },
    {
      "timestamp": "2017-03-09T19:22:03+0000",
      "principal": "user",
      "type": "AUTHENTICATION_SUCCESS",
      "data": {
        "details": {
          "remoteAddress": "0:0:0:0:0:0:0:1",
          "sessionId": "18FA15865F80760521BBB736D3036901"
        }
      }
    }
  ]
}

6. Conclusion

With the actuator support in Spring Boot, it becomes trivial to log the authentication and authorization attempts from users. The reader is also referred to production ready auditing for some additional information.

The code from this article can be found over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!