From d2148ac5ae60b82568dd4918afdad2a21dd800cd Mon Sep 17 00:00:00 2001 From: Matthijs Date: Thu, 10 Oct 2024 16:28:56 +0200 Subject: [PATCH] HTM-1161: Add authentication event logging (#976) * Add authentication event logging * Improve auth logging --- .../security/ApiSecurityConfiguration.java | 23 +++++ .../api/security/AuthenticationEvents.java | 86 +++++++++++++++++++ .../DefaultAuthenticationFailureEvent.java | 19 ++++ .../OAuth2AuthenticationFailureEvent.java | 19 ++++ src/main/resources/application.properties | 1 + 5 files changed, 148 insertions(+) create mode 100644 src/main/java/org/tailormap/api/security/AuthenticationEvents.java create mode 100644 src/main/java/org/tailormap/api/security/events/DefaultAuthenticationFailureEvent.java create mode 100644 src/main/java/org/tailormap/api/security/events/OAuth2AuthenticationFailureEvent.java diff --git a/src/main/java/org/tailormap/api/security/ApiSecurityConfiguration.java b/src/main/java/org/tailormap/api/security/ApiSecurityConfiguration.java index cb230c55c..9c2719fd4 100644 --- a/src/main/java/org/tailormap/api/security/ApiSecurityConfiguration.java +++ b/src/main/java/org/tailormap/api/security/ApiSecurityConfiguration.java @@ -9,14 +9,18 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import java.io.IOException; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.server.Cookie; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationEventPublisher; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -24,6 +28,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.web.DefaultRedirectStrategy; @@ -37,6 +42,8 @@ import org.tailormap.api.persistence.Group; import org.tailormap.api.repository.GroupRepository; import org.tailormap.api.repository.OIDCConfigurationRepository; +import org.tailormap.api.security.events.DefaultAuthenticationFailureEvent; +import org.tailormap.api.security.events.OAuth2AuthenticationFailureEvent; @Configuration @EnableWebSecurity @@ -69,6 +76,22 @@ public CookieCsrfTokenRepository csrfTokenRepository() { return csrfTokenRepository; } + @Bean + public AuthenticationEventPublisher authenticationEventPublisher( + ApplicationEventPublisher applicationEventPublisher) { + DefaultAuthenticationEventPublisher authenticationEventPublisher = + new DefaultAuthenticationEventPublisher(applicationEventPublisher); + + authenticationEventPublisher.setAdditionalExceptionMappings( + Collections.singletonMap( + OAuth2AuthenticationException.class, OAuth2AuthenticationFailureEvent.class)); + + authenticationEventPublisher.setDefaultAuthenticationFailureEvent( + DefaultAuthenticationFailureEvent.class); + + return authenticationEventPublisher; + } + @Bean public SecurityFilterChain apiFilterChain( HttpSecurity http, CookieCsrfTokenRepository csrfTokenRepository) throws Exception { diff --git a/src/main/java/org/tailormap/api/security/AuthenticationEvents.java b/src/main/java/org/tailormap/api/security/AuthenticationEvents.java new file mode 100644 index 000000000..fa487a6f4 --- /dev/null +++ b/src/main/java/org/tailormap/api/security/AuthenticationEvents.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 B3Partners B.V. + * + * SPDX-License-Identifier: MIT + */ + +package org.tailormap.api.security; + +import java.lang.invoke.MethodHandles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.EventListener; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.event.AbstractAuthenticationEvent; +import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.stereotype.Component; +import org.tailormap.api.security.events.DefaultAuthenticationFailureEvent; +import org.tailormap.api.security.events.OAuth2AuthenticationFailureEvent; + +@Component +public class AuthenticationEvents { + private static final Logger logger = + LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static String getExtraInfo(AbstractAuthenticationEvent event) { + String extraInfo = ""; + if (logger.isTraceEnabled() + && event.getAuthentication().getDetails() instanceof WebAuthenticationDetails details) { + extraInfo = String.format(" (IP: %s)", details.getRemoteAddress()); + } + return extraInfo; + } + + @EventListener + public void onSuccess(AuthenticationSuccessEvent success) { + String authInfo = ""; + if (success.getSource() instanceof OAuth2LoginAuthenticationToken token) { + String userClaims = ""; + if (token.getPrincipal() instanceof DefaultOidcUser oidcUser) { + userClaims = ", user claims: " + oidcUser.getUserInfo().getClaims(); + } + authInfo = + String.format( + "via OIDC registration \"%s\" with client ID %s%s", + token.getClientRegistration().getClientName(), + token.getClientRegistration().getClientId(), + userClaims); + } + if (success.getSource() instanceof UsernamePasswordAuthenticationToken) { + authInfo = "using username/password"; + } + logger.info( + "Authentication successful for user \"{}\"{}, granted authorities: {}, {}", + success.getAuthentication().getName(), + getExtraInfo(success), + success.getAuthentication().getAuthorities().toString(), + authInfo); + } + + @EventListener + public void onFailure(AbstractAuthenticationFailureEvent failure) { + String userInfo = ""; + if (failure.getAuthentication().getPrincipal() != null) { + userInfo = String.format(" for user \"%s\"", failure.getAuthentication().getPrincipal()); + } + logger.info( + "Authentication failure: {} {}{}", + failure.getException().getMessage(), + userInfo, + getExtraInfo(failure)); + } + + @EventListener + public void onOAuth2AuthenticationFailureEvent(OAuth2AuthenticationFailureEvent event) { + logger.info("OAuth2 authentication failure: {}, {}", event.getException().getMessage(), event); + } + + @EventListener + public void onDefaultAuthenticationFailureEvent(DefaultAuthenticationFailureEvent event) { + logger.info("Default authentication failure", event.getException()); + } +} diff --git a/src/main/java/org/tailormap/api/security/events/DefaultAuthenticationFailureEvent.java b/src/main/java/org/tailormap/api/security/events/DefaultAuthenticationFailureEvent.java new file mode 100644 index 000000000..ddeb3ed2d --- /dev/null +++ b/src/main/java/org/tailormap/api/security/events/DefaultAuthenticationFailureEvent.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 B3Partners B.V. + * + * SPDX-License-Identifier: MIT + */ + +package org.tailormap.api.security.events; + +import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +public class DefaultAuthenticationFailureEvent extends AbstractAuthenticationFailureEvent { + + public DefaultAuthenticationFailureEvent( + Authentication authentication, AuthenticationException exception) { + super(authentication, exception); + } +} diff --git a/src/main/java/org/tailormap/api/security/events/OAuth2AuthenticationFailureEvent.java b/src/main/java/org/tailormap/api/security/events/OAuth2AuthenticationFailureEvent.java new file mode 100644 index 000000000..26a28de4e --- /dev/null +++ b/src/main/java/org/tailormap/api/security/events/OAuth2AuthenticationFailureEvent.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 B3Partners B.V. + * + * SPDX-License-Identifier: MIT + */ + +package org.tailormap.api.security.events; + +import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +public class OAuth2AuthenticationFailureEvent extends AbstractAuthenticationFailureEvent { + + public OAuth2AuthenticationFailureEvent( + Authentication authentication, AuthenticationException exception) { + super(authentication, exception); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9f96699a6..273aebeea 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -120,3 +120,4 @@ logging.include-application-name=false logging.group.tailormap=org.tailormap logging.group.geotools=org.geotools +logging.group.auth=org.tailormap.api.security.AuthenticationEvents