diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index fd779ebc95..5c8b63421c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.log.LogMessage; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; @@ -79,8 +80,7 @@ public class AuthenticationConfiguration { public AuthenticationManagerBuilder authenticationManagerBuilder(ObjectPostProcessor objectPostProcessor, ApplicationContext context) { LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context); - AuthenticationEventPublisher authenticationEventPublisher = getBeanOrNull(context, - AuthenticationEventPublisher.class); + AuthenticationEventPublisher authenticationEventPublisher = getAuthenticationEventPublisher(context); DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder( objectPostProcessor, defaultPasswordEncoder); if (authenticationEventPublisher != null) { @@ -142,6 +142,13 @@ public class AuthenticationConfiguration { this.objectPostProcessor = objectPostProcessor; } + private AuthenticationEventPublisher getAuthenticationEventPublisher(ApplicationContext context) { + if (context.getBeanNamesForType(AuthenticationEventPublisher.class).length > 0) { + return context.getBean(AuthenticationEventPublisher.class); + } + return this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher()); + } + @SuppressWarnings("unchecked") private T lazyBean(Class interfaceName) { LazyInitTargetSource lazyTargetSource = new LazyInitTargetSource(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 468ba74bf5..ed9ce6d800 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -26,7 +26,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; @@ -85,6 +87,7 @@ class HttpSecurityConfiguration { AuthenticationManagerBuilder authenticationBuilder = new WebSecurityConfigurerAdapter.DefaultPasswordEncoderAuthenticationManagerBuilder( this.objectPostProcessor, passwordEncoder); authenticationBuilder.parentAuthenticationManager(authenticationManager()); + authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher()); HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects()); // @formatter:off http @@ -109,6 +112,13 @@ class HttpSecurityConfiguration { : this.authenticationConfiguration.getAuthenticationManager(); } + private AuthenticationEventPublisher getAuthenticationEventPublisher() { + if (this.context.getBeanNamesForType(AuthenticationEventPublisher.class).length > 0) { + return this.context.getBean(AuthenticationEventPublisher.class); + } + return this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher()); + } + private void applyDefaultConfigurers(HttpSecurity http) throws Exception { ClassLoader classLoader = this.context.getClassLoader(); List defaultHttpConfigurers = SpringFactoriesLoader diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java index 243bb0284e..ffaab17e87 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java @@ -34,8 +34,10 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -51,6 +53,7 @@ import org.springframework.security.config.annotation.web.servlet.configuration. import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.users.AuthenticationTestConfiguration; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; @@ -62,6 +65,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -296,6 +300,28 @@ public class AuthenticationConfigurationTests { assertThatExceptionOfType(AlreadyBuiltException.class).isThrownBy(ap::build); } + @Test + public void configureWhenDefaultsThenDefaultAuthenticationEventPublisher() { + this.spring.register(AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class).autowire(); + AuthenticationManagerBuilder authenticationManagerBuilder = this.spring.getContext() + .getBean(AuthenticationManagerBuilder.class); + AuthenticationEventPublisher eventPublisher = (AuthenticationEventPublisher) ReflectionTestUtils + .getField(authenticationManagerBuilder, "eventPublisher"); + assertThat(eventPublisher).isInstanceOf(DefaultAuthenticationEventPublisher.class); + } + + @Test + public void configureWhenCustomAuthenticationEventPublisherThenCustomAuthenticationEventPublisher() { + this.spring.register(AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class, + CustomAuthenticationEventPublisherConfig.class).autowire(); + AuthenticationManagerBuilder authenticationManagerBuilder = this.spring.getContext() + .getBean(AuthenticationManagerBuilder.class); + AuthenticationEventPublisher eventPublisher = (AuthenticationEventPublisher) ReflectionTestUtils + .getField(authenticationManagerBuilder, "eventPublisher"); + assertThat(eventPublisher) + .isInstanceOf(CustomAuthenticationEventPublisherConfig.MyAuthenticationEventPublisher.class); + } + @EnableGlobalMethodSecurity(securedEnabled = true) static class GlobalMethodSecurityAutowiredConfig { @@ -348,6 +374,30 @@ public class AuthenticationConfigurationTests { } + @Configuration + static class CustomAuthenticationEventPublisherConfig { + + @Bean + AuthenticationEventPublisher eventPublisher() { + return new MyAuthenticationEventPublisher(); + } + + static class MyAuthenticationEventPublisher implements AuthenticationEventPublisher { + + @Override + public void publishAuthenticationSuccess(Authentication authentication) { + + } + + @Override + public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) { + + } + + } + + } + interface Service { void run(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java index 030f8f2a7e..8713b17121 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java @@ -16,7 +16,9 @@ package org.springframework.security.config.annotation.web.configuration; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.concurrent.Callable; import javax.servlet.http.HttpServletRequest; @@ -32,14 +34,21 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.TestingAuthenticationToken; +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.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -56,6 +65,7 @@ import org.springframework.web.bind.annotation.RestController; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; @@ -211,6 +221,48 @@ public class HttpSecurityConfigurationTests { this.mockMvc.perform(get("/login?logout")).andExpect(status().isOk()); } + @Test + public void loginWhenUsingDefaultThenAuthenticationEventPublished() throws Exception { + this.spring + .register(SecurityEnabledConfig.class, UserDetailsConfig.class, AuthenticationEventListenerConfig.class) + .autowire(); + AuthenticationEventListenerConfig.clearEvents(); + this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection()); + assertThat(AuthenticationEventListenerConfig.EVENTS).isNotEmpty(); + assertThat(AuthenticationEventListenerConfig.EVENTS).hasSize(1); + } + + @Test + public void loginWhenUsingDefaultAndNoUserDetailsServiceThenAuthenticationEventPublished() throws Exception { + this.spring + .register(SecurityEnabledConfig.class, UserDetailsConfig.class, AuthenticationEventListenerConfig.class) + .autowire(); + AuthenticationEventListenerConfig.clearEvents(); + this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection()); + assertThat(AuthenticationEventListenerConfig.EVENTS).isNotEmpty(); + assertThat(AuthenticationEventListenerConfig.EVENTS).hasSize(1); + } + + @Test + public void loginWhenUsingCustomAuthenticationEventPublisherThenAuthenticationEventPublished() throws Exception { + this.spring.register(SecurityEnabledConfig.class, UserDetailsConfig.class, + CustomAuthenticationEventPublisherConfig.class).autowire(); + CustomAuthenticationEventPublisherConfig.clearEvents(); + this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection()); + assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).isNotEmpty(); + assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).hasSize(1); + } + + @Test + public void loginWhenUsingCustomAuthenticationEventPublisherAndNoUserDetailsServiceThenAuthenticationEventPublished() + throws Exception { + this.spring.register(SecurityEnabledConfig.class, CustomAuthenticationEventPublisherConfig.class).autowire(); + CustomAuthenticationEventPublisherConfig.clearEvents(); + this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection()); + assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).isNotEmpty(); + assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).hasSize(1); + } + @Test public void configureWhenAuthorizeHttpRequestsBeforeAuthorizeRequestThenException() { assertThatExceptionOfType(BeanCreationException.class) @@ -348,6 +400,55 @@ public class HttpSecurityConfigurationTests { } + @Configuration + static class CustomAuthenticationEventPublisherConfig { + + static List EVENTS = new ArrayList<>(); + + static void clearEvents() { + EVENTS.clear(); + } + + @Bean + AuthenticationEventPublisher publisher() { + return new AuthenticationEventPublisher() { + + @Override + public void publishAuthenticationSuccess(Authentication authentication) { + EVENTS.add(authentication); + } + + @Override + public void publishAuthenticationFailure(AuthenticationException exception, + Authentication authentication) { + EVENTS.add(authentication); + } + }; + } + + } + + @Configuration + static class AuthenticationEventListenerConfig { + + static List EVENTS = new ArrayList<>(); + + static void clearEvents() { + EVENTS.clear(); + } + + @EventListener + void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { + EVENTS.add(event); + } + + @EventListener + void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { + EVENTS.add(event); + } + + } + @RestController static class BaseController {