StackOverflowError when trying to route to Login page, possibly due to Spri

I have an application that is currently secured with Spring Security and using the Spring Security login form, but I wanted to switch to using the new to Vaadin 13+ login addon. I’m running into an odd error during setup.

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.company.inventoryapp.LoginView': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.company.inventoryapp.LoginView]
: Constructor threw exception; nested exception is java.lang.StackOverflowError
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1287)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:305)
	at com.vaadin.flow.spring.SpringInstantiator.getOrCreate(SpringInstantiator.java:88)
	at com.vaadin.flow.di.Instantiator.createRouteTarget(Instantiator.java:158)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.lambda$getRouteTarget$1(AbstractNavigationStateRenderer.java:121)
	at java.util.Optional.orElseGet(Optional.java:267)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.getRouteTarget(AbstractNavigationStateRenderer.java:120)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handle(AbstractNavigationStateRenderer.java:178)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.reroute(AbstractNavigationStateRenderer.java:385)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handle(AbstractNavigationStateRenderer.java:218)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.reroute(AbstractNavigationStateRenderer.java:385)
 ... the AbstractNavigationStateRenderer line repeats for a while...
 Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.elavon.inventoryapp.LoginView]
: Constructor threw exception; nested exception is java.lang.StackOverflowError
	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:184)
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:87)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1279)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:305)
	at com.vaadin.flow.spring.SpringInstantiator.getOrCreate(SpringInstantiator.java:88)
	at com.vaadin.flow.di.Instantiator.createRouteTarget(Instantiator.java:158)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.lambda$getRouteTarget$1(AbstractNavigationStateRenderer.java:121)
	at java.util.Optional.orElseGet(Optional.java:267)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.getRouteTarget(AbstractNavigationStateRenderer.java:120)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handle(AbstractNavigationStateRenderer.java:178)
	... 1011 more
Caused by: java.lang.StackOverflowError
	at java.lang.reflect.InvocationTargetException.<init>(InvocationTargetException.java:72)
	at sun.reflect.GeneratedConstructorAccessor52.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.security.Provider$Service.newInstance(Provider.java:1595)
	at sun.security.jca.GetInstance.getInstance(GetInstance.java:236)
	at sun.security.jca.GetInstance.getInstance(GetInstance.java:164)
	at java.security.Security.getImpl(Security.java:730)
	at java.security.MessageDigest.getInstance(MessageDigest.java:167)
	at com.vaadin.flow.internal.MessageDigestUtil.getSha256(MessageDigestUtil.java:49)
	at com.vaadin.flow.internal.MessageDigestUtil.sha256(MessageDigestUtil.java:44)
	at com.vaadin.flow.internal.ConstantPoolKey.calculateHash(ConstantPoolKey.java:94)
	at com.vaadin.flow.internal.ConstantPoolKey.<init>(ConstantPoolKey.java:55)
	at com.vaadin.flow.internal.nodefeature.ElementListenerMap.updateEventSettings(ElementListenerMap.java:362)
	at com.vaadin.flow.internal.nodefeature.ElementListenerMap.add(ElementListenerMap.java:291)
	at com.vaadin.flow.dom.impl.BasicElementStateProvider.addEventListener(BasicElementStateProvider.java:237)
	at com.vaadin.flow.dom.Element.addEventListener(Element.java:513)
	at com.vaadin.flow.component.ComponentEventBus.addDomTrigger(ComponentEventBus.java:262)
	at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTriggerIfNeeded$1(ComponentEventBus.java:226)
	at java.util.Optional.map(Optional.java:215)
	at com.vaadin.flow.component.ComponentEventBus.addDomTriggerIfNeeded(ComponentEventBus.java:225)
	at com.vaadin.flow.component.ComponentEventBus.addListenerInternal(ComponentEventBus.java:143)
	at com.vaadin.flow.component.ComponentEventBus.addListener(ComponentEventBus.java:101)
	at com.vaadin.flow.component.Component.addListener(Component.java:331)
	at com.vaadin.flow.component.ComponentUtil.addListener(ComponentUtil.java:338)
	at com.vaadin.flow.component.login.AbstractLogin.addLoginListener(AbstractLogin.java:161)
	at com.vaadin.flow.component.login.AbstractLogin.<init>(AbstractLogin.java:63)
	at com.vaadin.flow.component.login.LoginForm.<init>(LoginForm.java:35)
	at com.elavon.inventoryapp.LoginView.<init>(LoginView.java:34)
	at sun.reflect.GeneratedConstructorAccessor57.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:172)
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:87)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1279)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:305)
	at com.vaadin.flow.spring.SpringInstantiator.getOrCreate(SpringInstantiator.java:88)
	at com.vaadin.flow.di.Instantiator.createRouteTarget(Instantiator.java:158)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.lambda$getRouteTarget$1(AbstractNavigationStateRenderer.java:121)
	at java.util.Optional.orElseGet(Optional.java:267)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.getRouteTarget(AbstractNavigationStateRenderer.java:120)
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handle(AbstractNavigationStateRenderer.java:178)
	... 979 more

My SecurityConfig

@Configuration 
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	private static final String LOGIN_PROCESSING_URL = "/login";
	private static final String LOGIN_FAILURE_URL = "/login";
	private static final String LOGIN_URL = "/login";
	private static final String LOGOUT_SUCCESS_URL = "/";

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		/**
		 * Not using Spring CSRF here to be able to use plain HTML for the login page, Vaadin has its own CSRF protection
		 */
		http.csrf().disable()
			// Register our CustomRequestCache, that saves unauthorized access attempts, so
			// the user is redirected after login.
			.requestCache().requestCache(new CustomRequestCache())
		.and()
			.authorizeRequests()
				// Allow all flow internal requests.
				.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
				// Allow all requests by logged in users.
				.anyRequest().authenticated()
		.and()
			// Sets up login page
			.formLogin().loginPage(LOGIN_URL).permitAll().loginProcessingUrl(LOGIN_PROCESSING_URL)
			.failureUrl(LOGIN_FAILURE_URL)
			// Register the success handler that redirects users to the page they last tried to access
			.successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
		.and()
			// Configure logout
			.logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL).permitAll()
		.and()
			// Limits users to a single session
			.sessionManagement().maximumSessions(1)

		;
	}

	// ? Consider allowing for login from a created user, not just an LDAP authenticated user
	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.ldapAuthentication()
			.userDnPatterns("uid={0},ou=people")
			.groupSearchBase("ou=groups")
			.contextSource()
				.url("ldap://localhost:8389/dc=springframework,dc=org")
			.and()
				.passwordCompare()
					// ! Deprecated as insecure (see import), should only be needed during dev for test data
					.passwordEncoder(new LdapShaPasswordEncoder())
					.passwordAttribute("userPassword")
			.and()
				.ldapAuthoritiesPopulator(authPopulator())
		;
	}

	@Bean(name = "ldapAuthoritiesPopulator")
    public CustomLdapAuthoritiesPopulator authPopulator() {
        CustomLdapAuthoritiesPopulator populator = new CustomLdapAuthoritiesPopulator();
        return populator;
	}

	/**
	 * Allows access to static resources, bypassing Spring security.
	 */
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers(
				// Vaadin Flow static resources //
				"/VAADIN/**",

				// the standard favicon URI
				"/favicon.ico",

				// the robots exclusion standard
				"/robots.txt",

				// web application manifest //
				"/manifest.webmanifest",
				"/sw.js",
				"/offline-page.html",

				// (development mode) static resources //
				"/frontend/**",

				// (development mode) webjars //
				"/webjars/**",

				// (production mode) static resources //
				"/frontend-es5/**", "/frontend-es6/**");
	}
}

and my LoginView

@Route(value = "login")
@PageTitle("Login")
public class LoginView extends VerticalLayout {

        private LoginForm login = new LoginForm(); //

        public LoginView(){
                login.setAction("login"); //
                getElement().appendChild(login.getElement()); //
        }

}

I’m fairly certain it has something to do with how I have the LDAP authentication set up, but I can’t figure out what to change. The Spring login form works perfectly.

Of course, the solution was fairly simple. In SecurityUtils (see the line in SecurityConfig about internal flow requests) there’s this

    /**
	 * Checks if access is granted for the current user for the given secured view, defined by the view class.
	 *
	 * @param securedClass View class
	 * @return true if access is granted, false otherwise.
	 */
	public static boolean isAccessGranted(Class<?> securedClass) {
        final boolean publicView =
                DeviceView.class.equals(securedClass)
             // || LoginView.class.equals(securedClass)
			  || AccessDeniedView.class.equals(securedClass);

		// Always allow access to public views
		if (publicView) {
			return true;
		}

Note that I’d commented out the LoginView.class exception when setting it up, knowing I’d come back to setting up a login page later. Getting rid of the comment made everything work correctly.