Search code examples
javaspring-bootspring-security

How to trigger custom AuthenticationProvider for Cookie based authentication in Spring Boot?


I'm implementing the flow when user obtains cookie by one-time temporary URL and accesses protected resource using the cookie as authentication token

For this purpose there is CookieAuthenticationFilter, UserAuthProvider and SecurityConfig:

CookieAuthenticationFilter

public class CookieAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
        FilterChain filterChain) throws ServletException, IOException {
            Cookie cookie = Stream.of(Optional.ofNullable(request.getCookies())
                .orElse(new Cookie[0]))
                .filter(entry -> "token".equals(entry.getName()))
                .findFirst()
                .orElse(null);
           
            SecurityContextHolder.getContext()
                .setAuthentication(new PreAuthenticatedAuthenticationToken(
                    cookie != null ? cookie.getValue() : "",
                    null));

            filterChain.doFilter(request, response);
    }

}

UserAuthProvider

@Component
public class UserAuthProvider implements AuthenticationProvider {

    private final AuthenticationService authenticationService;

    public UserAuthProvider(AuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        User user = null;
        if (authentication instanceof PreAuthenticatedAuthenticationToken) {
            user = authenticationService.validateUser((String) authentication.getPrincipal());
        }

        return user != null ? new UserAuthenticationToken(user) : null;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }

}

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, UserAuthProvider userAuthProvider) throws Exception {
        AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);
        builder.authenticationProvider(userAuthProvider);
        return builder.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity security, HttpSession session) throws Exception {  
        security
            .csrf(customizer -> customizer.disable())
            .addFilterBefore(new CookieAuthenticationFilter(), BasicAuthenticationFilter.class)
            .authorizeHttpRequests(configurer -> configurer
                .requestMatchers("/*").authenticated()
                .anyRequest().permitAll())
            .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return security.build();
    }

}

Thing is that authenticate method of UserAuthProvided is not triggered

Spring Boot 3.3.1

  1. What do I miss?
  2. Is there a way to resolve path variable inside filter?
  3. Is it good practice to implement authentication in interceptors layer?

Thanks!


Solution

  • Currently your filter is flawed as you are retrieving and directly setting the SecurityContext. What you should do is call the AuthenticationManager.authenticate method with the token you create. That in turn will trigger your AuthenticationProvider.

    Another thing you can do to make things a bit easier is to extend the AbstractPreAuthenticatedProcessingFilter and provide an AuthenticationUserDetailsService for integration with the standard PreAuthenticatedAuthenticationProvider. This will leverage more of the pre-build components instead of re-inventing all of it yourself.

    Your filter would look something like this.

    public class CookieAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
      @Override
      protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        var cookie = WebUtils.getCookie(request, "token");
        return cookie != null ? cookie.getValue() : null;
      }
    
      @Override
      protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        return "";
      }
    }
    

    Then you would need an AuthenticationUserDetailsService which delegates to your AuthenticationService.

    public class CookieAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
    
      private final AuthenticationSerivce authenticationSerivce;
    
      public CookieAuthenticationUserDetailsService(AuthenticationSerivce authenticationSerivce) {
        this.authenticationSerivce = authenticationSerivce;
      }
    
      @Override
      public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) throws UsernameNotFoundException {
        // load user from service.
        return null;
      }
    }
    

    Finally you need to configure all this. When doing complex configuration it is often easier to write a Configurer then to try to access all the beans. The following configurer would configure your pre-auth.

    public class CookieAuthenticationConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<JeeConfigurer<H>, H> {
    
      private CookieAuthenticationFilter authenticationFilter;
      private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService;
    
      public void init(H http) {
        PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
        authenticationProvider.setPreAuthenticatedUserDetailsService(this.getUserDetailsService());
        authenticationProvider = this.postProcess(authenticationProvider);
        http.authenticationProvider(authenticationProvider).setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
      }
    
      public void configure(H http) {
        CookieAuthenticationFilter filter = this.getFilter(http.getSharedObject(AuthenticationManager.class), http);
        http.addFilter(filter);
      }
    
      public CookieAuthenticationConfigurer<H> authenticatedUserDetailsService(AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticatedUserDetailsService) {
        this.authenticationUserDetailsService = authenticatedUserDetailsService;
        return this;
      }
    
    
      private CookieAuthenticationFilter getFilter(AuthenticationManager authenticationManager, H http) {
        if (this.authenticationFilter == null) {
          this.authenticationFilter = new CookieAuthenticationFilter();
          this.authenticationFilter.setAuthenticationManager(authenticationManager);
          this.authenticationFilter.setSecurityContextHolderStrategy(this.getSecurityContextHolderStrategy());
          this.authenticationFilter = this.postProcess(this.authenticationFilter);
        }
        return this.authenticationFilter;
      }
    
      private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> getUserDetailsService() {
        return this.authenticationUserDetailsService != null ? this.authenticationUserDetailsService : new PreAuthenticatedGrantedAuthoritiesUserDetailsService();
      }
    
    }
    

    The beauty of this is you don't need to expose the shared Spring Security objects like the AuthenticationManager etc. Not doing so also saves you from risking to eagerly instantiate beans and leading to startup issues. Next to that the use of the standard components also allows for better integration with Spring / Spring Security as it will also call success/failure handlers, fire events etc. etc.

    Your SecurityConfig can use this configurer as follows.

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
    
      @Bean
      public SecurityFilterChain securityFilterChain(HttpSecurity security, AuthenticationSerivce authService) throws Exception {
        security
            .csrf(AbstractHttpConfigurer::disable)
            .with(new CookieAuthenticationConfigurer<>(), (pre) -> pre.authenticatedUserDetailsService(new CookieAuthenticationUserDetailsService(authService)))
            .authorizeHttpRequests(configurer -> configurer
                .requestMatchers("/*").authenticated()
                .anyRequest().permitAll())
            .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    
        return security.build();
      }
    }
    

    Notice the with part, that is all you need to register your filter etc. no need to declare @Bean methods to expose shared objects etc.