Spring Security - Prevent Authenticated Users From Accessing Login Page

After browsing StackOverflow a bit, I stumbled upon a pretty interesting question:

Redirecting from /login to / for authenticated users

You’d think that this question would have been answered before, but to my surprise, it seems like it hasn’t.

By default, you can access the login page even if you’re already authenticated.

While for most, this isn’t an issue, it can be problematic for others, so I came up with a solution to this problem.

The problem is that even if you add

.authorizeRequests().antMatchers("/login").not().authenticated()

To your HttpSecurity configuration, it’s still not going to work because the filter that generates the login page is higher than the filter that processes the HttpSecurity matchers.

The solution is, as you may have guessed, to add a filter of our own before the filter generating the login page.

First, we need to create a custom filter class, I named it LoginPageFilter:

class LoginPageFilter extends GenericFilterBean {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (SecurityContextHolder.getContext().getAuthentication() != null
              && SecurityContextHolder.getContext().getAuthentication().isAuthenticated()
              && ((HttpServletRequest)request).getRequestURI().equals("/login")) {
            System.out.println("user is authenticated but trying to access login page, redirecting to /");
            ((HttpServletResponse)response).sendRedirect("/");
        }
        chain.doFilter(request, response);
    }
    
}

After, we need to add the filter to the existing filter chain, but we can’t add it anywhere. It has to be after the authentication has been resolved by the session id (otherwise, SecurityContextHolder.getContext().getAuthentication() would always return null) and it has to be before the existing filter that generates the login page.

The best candidate is before UsernamePasswordAuthenticationFilter: due to the fact that we’re checking if the user is authenticated, (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()), we can safely assume that the user does not need to be processed in UsernamePasswordAuthenticationFilter (since he’s already logged in!).

To add the custom filter at that position, we need to add this at the top of our configure(HttpSecurity http) method:

http.addFilterBefore(new LoginPageFilter(), UsernamePasswordAuthenticationFilter.class);

So, a rough but complete implementation of the above would look like this:

package org.twinnation.stackoverflowspring.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	class LoginPageFilter extends GenericFilterBean {
		
		@Override
		public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
			if (SecurityContextHolder.getContext().getAuthentication() != null
				  && SecurityContextHolder.getContext().getAuthentication().isAuthenticated()
				  && ((HttpServletRequest)request).getRequestURI().equals("/login")) {
				System.out.println("user is authenticated but trying to access login page, redirecting to /");
				((HttpServletResponse)response).sendRedirect("/");
			}
			chain.doFilter(request, response);
		}
		
	}
	
	
	@Override
	protected void configure(HttpSecurity httpSecurity) throws Exception {
		httpSecurity.addFilterBefore(
			new LoginPageFilter(), DefaultLoginPageGeneratingFilter.class);
		
		httpSecurity.httpBasic()
			.and()
			.csrf().disable()
			.headers().frameOptions().sameOrigin()
			.and().formLogin().permitAll()
			.and().authorizeRequests().antMatchers("/login").not().authenticated()
			.and()
			.authorizeRequests()
				.anyRequest().authenticated();
	}
	
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication()
			.withUser("root").password(passwordEncoder().encode("root")).roles("USER");
	}

}

And that’s about it! You should now be redirected if you’re authenticated while trying to access /login.