1

For integration of Spring Boot with htmx, I need a way to add a header if an incoming request is done by htmx and the user is no longer logged on.

In the normal flow, the user gets redirected to the login page. However, when there is a request done by htmx, this is an AJAX request and the redirect is not happening.

The recommended solution is that if there is an HX-Request header on the request, that the server should put an HX-Refresh: true header on the response. This will make htmx do a full page refresh.

My security config looks like this:

@Configuration
public class WebSecurityConfiguration {
    private final ClientRegistrationRepository clientRegistrationRepository;

    public WebSecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(registry -> {
            registry.mvcMatchers("/actuator/info", "/actuator/health").permitAll();
            registry.mvcMatchers("/**").hasAuthority(Roles.ADMIN);
            registry.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
            registry.anyRequest().authenticated();
        });
        http.oauth2Client();
        http.oauth2Login();
        http.logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));

        return http.build();
    }

    private LogoutSuccessHandler oidcLogoutSuccessHandler() {
        OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);

        // Sets the location that the End-User's User Agent will be redirected to
        // after the logout has been performed at the Provider
        logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");

        return logoutSuccessHandler;
    }
}

I tried with a Filter:

public Filter htmxFilter() {
        return new Filter() {
            @Override
            public void doFilter(ServletRequest servletRequest,
                                 ServletResponse servletResponse,
                                 FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) servletRequest;
                HttpServletResponse response = (HttpServletResponse) servletResponse;

                filterChain.doFilter(servletRequest, servletResponse);
                String htmxRequestHeader = request.getHeader("HX-Request");
                System.out.println("htmxRequestHeader = " + htmxRequestHeader);
                System.out.println(response.getStatus());
                if (htmxRequestHeader != null
                        && response.getStatus() == 302) {
                    System.out.println("XXXXXXXXXXX");
                    response.setHeader("HX-Refresh", "true");
                }
            }
        };
    }

But response.getStatus() is never 302 (altough I can see the 302 response status in Chrome).

I also tried with an Interceptor:

    @Bean
    public HandlerInterceptor htmxHandlerInterceptor() {
        return new HandlerInterceptor() {

            @Override
            public void postHandle(HttpServletRequest request,
                                   HttpServletResponse response,
                                   Object handler,
                                   ModelAndView modelAndView) throws Exception {
                boolean htmxRequest = request.getHeader("HX-Request") != null;
                String htmxRequestHeader = request.getHeader("HX-Request");
                System.out.println("htmxRequestHeader = " + htmxRequestHeader);
                System.out.println(response.getStatus());

                if( htmxRequest && response.getStatus() == 302) {
                    response.setHeader("HX-Refresh", "true");
                }
            }
        };
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeInterceptor());
        registry.addInterceptor(htmxHandlerInterceptor());//.order(Ordered.HIGHEST_PRECEDENCE);
    }
    

Which also does not work, there is no 302 response status.

I also tried with the commented out order(Ordered.HIGHEST_PRECEDENCE), but that did not make any difference.

Are there other options?

Wim Deblauwe
  • 25,113
  • 20
  • 133
  • 211
  • I'm not sure it is a 302 that is being generated eventually maybe but could be a 301 or 303 as well. You could try using the `HttpStatus` enum and check for `.is3xxRedirection` else debug what is happening (enable trace logging). I also think this will only work with a `Filter` not a `HandlerInterceptor` as the request is probably blocked by Spring Security which is a Filter as well. So it never passes on and reaches a `DispatcherServlet`. – M. Deinum Sep 22 '22 at 11:38
  • I could not understand exactly what are the circumstances to add that `HX-Refresh` header. Is it upon a logout request or the header can be added to any request if the request contains `HX-Request` and the user is not authenticated? – Marcus Hert da Coregio Sep 22 '22 at 12:23
  • @MarcusHertdaCoregio that is exactly the problem indeed. The session of the user might be expired, or the user might have logged out in a different tab for example. – Wim Deblauwe Sep 22 '22 at 12:49

2 Answers2

1

When a request comes to a protected endpoint and it is not authenticated, Spring Security executes its AuthenticationEntryPoints interface to commence an authentication scheme.

You could create your own AuthenticationEntryPoint that adds the header and delegates to the LoginUrlAuthenticationEntryPoint (or other implementation that you are using).

@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
    http
       //...
       .exceptionHandling(exception -> exception
           .authenticationEntryPoint(new HxRefreshHeaderAuthenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
       );
    return http.build();
}

public class HxRefreshHeaderAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final AuthenticationEntryPoint delegate;

    public HxRefreshHeaderAuthenticationEntryPoint(AuthenticationEntryPoint delegate) {
        this.delegate = delegate;
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        
        // Add the header

        this.delegate.commence(request, response, authException);
    }
}
0

You need to make sure that your filter runs before any Spring Security filter. See at SecurityProperties.DEFAULT_FILTER_ORDER or HttpSecurity#addFilterBefore

Maciej Walkowiak
  • 12,372
  • 59
  • 63
  • Almost there :) It seems that setting a header after delegating to `doFilter` does not actually get set on the response :( – Wim Deblauwe Sep 22 '22 at 12:48
  • https://stackoverflow.com/questions/55962059/setting-a-response-header-after-dofilter-call will probably be the way to fix that. – Wim Deblauwe Sep 22 '22 at 12:51
  • In case anybody else wonders, this is the correct form to use with `addFilterBefore`: `http.addFilterBefore(htmxFilter(), UsernamePasswordAuthenticationFilter.class);` – Wim Deblauwe Sep 22 '22 at 12:52