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 fd779ebc95e..5c8b63421ca 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.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 void setObjectPostProcessor(ObjectPostProcessor objectPostProcess 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 2aad5f658af..6baacd152e8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -24,7 +24,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +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; @@ -82,6 +84,7 @@ HttpSecurity httpSecurity() throws Exception { 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 @@ -105,6 +108,13 @@ private AuthenticationManager authenticationManager() throws Exception { : 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 Map, Object> createSharedObjects() { Map, Object> sharedObjects = new HashMap<>(); sharedObjects.put(ApplicationContext.class, this.context); 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 412768d1241..86666732f4d 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.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.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.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; @@ -294,6 +298,28 @@ public void getAuthenticationManagerWhenAuthenticationConfigurationSubclassedThe 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 { @@ -346,6 +372,30 @@ Service service() { } + @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 813723e2839..5953b1e05b2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -16,6 +16,8 @@ package org.springframework.security.config.annotation.web.configuration; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Callable; import javax.servlet.http.HttpServletRequest; @@ -27,12 +29,19 @@ 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.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.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; @@ -48,6 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat; 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; @@ -200,6 +210,48 @@ public void loginWhenUsingDefaultsThenDefaultLogoutSuccessPageGenerated() throws 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); + } + @RestController static class NameController { @@ -270,6 +322,55 @@ UserDetailsService userDetailsService() { } + @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 {