Browse Source

Consistently set AuthenticationEventPublisher in AuthenticationManagerBuilder

Prior to this, the HttpSecurity bean was not consistent with WebSecurityConfigurerAdapter's HttpSecurity because it did not setup a default AuthenticationEventPublisher. This also fixes a problem where the AuthenticationEventPublisher bean would only be considered if there was a UserDetailsService

Closes gh-11449
Closes gh-11726
Marcus Da Coregio 3 years ago
parent
commit
a8d6c1d21f

+ 10 - 3
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<Object> 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> T lazyBean(Class<T> interfaceName) {
 		LazyInitTargetSource lazyTargetSource = new LazyInitTargetSource();

+ 11 - 1
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.ApplicationContext;
 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 @@ 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
@@ -105,6 +108,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 Map<Class<?>, Object> createSharedObjects() {
 		Map<Class<?>, Object> sharedObjects = new HashMap<>();
 		sharedObjects.put(ApplicationContext.class, this.context);

+ 50 - 0
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;
@@ -294,6 +298,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 {
 
@@ -346,6 +372,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();

+ 102 - 1
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.junit.jupiter.api.extension.ExtendWith;
 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 org.springframework.web.bind.annotation.RestController;
 
 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 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);
+	}
+
 	@RestController
 	static class NameController {
 
@@ -270,6 +322,55 @@ public class HttpSecurityConfigurationTests {
 
 	}
 
+	@Configuration
+	static class CustomAuthenticationEventPublisherConfig {
+
+		static List<Authentication> 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<AbstractAuthenticationEvent> 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 {