Search code examples
javaspring-bootspring-securitysession-cookiesunauthorized

Prevent unauthorized http requests redirected to /error from setting session cookie - spring boot - spring security


Context

I'm having some trouble with my application. We're using Spring Boot 2.4.10 and Spring Security 5.4.8. We use cookies to interact with the app.

We have a frontend application stored in src/main/resources that, among other things, connects to a websocket endpoint exposed in /api/v1/notification.

My configuration

application.properties file:

# cookie-related settings
server.servlet.session.cookie.name=mySessionCookie
server.servlet.session.cookie.path=/
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  private final String[] authorizedWithoutAuth = {
      "/",
      "/index.html",
      "/static/**"
  };

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic()
        .and()
          .authorizeRequests()
          .antMatchers(authorizedWithoutAuth).permitAll()
          .anyRequest().authenticated()
        .and()
          .csrf().disable()
        .and()
        .logout().logoutUrl("/api/v1/logout").invalidateHttpSession(true)
        .deleteCookies("mySessionCookie");
  }
}

The problem

While no user is logged in, the frontend tries to reach periodically the websocket endpoint to open a connection.

The first api/v1/notification ends redirected to the /error endpoint, which returns an HttpServletResponse with a 'Set-Cookie' header (I think this may be an anonymous cookie set in the first request?) and a 401 status. I cannot change this behaviour.

The following requests to api/v1/notification use this cookie in the header (while user is not logged in). These requests are also redirected to the /error endpoint, which returns each time an HttpServletResponse with 401 status but here, no 'Set-Cookie' header is included.

Once the user logs in with Authorization headers, a correct cookie is set by the response and used in the following requests.

The thing is, sometimes the set cookie suddenly changes again to an invalid one, and the following requests, done with this new invalid cookie, turn into a redirection to the login page.

After checking the code, it seems there is an old api/v1/notification request (previous to the login request) taking place, with an invalid cookie (the anonymous one, present before login).

This request is redirected to the /error endpoint: here, once again the HttpServletResponse has 401 status and is containing a Set-Cookie header that is modifying the browser cookie (replacing the good one).

Following is a scheme of the problem, to hopefully make it easier to understand.

enter image description here

Expected behaviour

I would like to prevent an unauthorized request from setting the session cookie.

It's ok if a previous request responds with a 401 code, but I don't want it to change the current set cookie.

I tried...

  • I tried extending the ErrorController by returning a ResponseEntity with all the headers present in the input HttpServletResponse except for the 'Set-Cookie' header. This doesn't work.

  • I also tried modifying my configuration to disable anonymous requests:

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
      private final String[] authorizedWithoutAuth = {
        "/",
        "/index.html",
        "/static/**"
      };
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()
            .and()
              .anonymous().disable()
              .authorizeRequests()
              // .antMatchers(authorizedWithoutAuth).permitAll() I had to remove these from here, and include them in the method below
              .anyRequest().authenticated()
            .and()
              .csrf().disable()
            .and()
              .logout().logoutUrl("/api/v1/logout").invalidateHttpSession(true)
              .deleteCookies("mySessionCookie");
      }
    
      @Override
      public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(authorizedWithoutAuth);
      }
    }
    

    but the session cookie is still set this way too, with 401 requests.

  • I also tried using @ControllerAdvice to handle the exceptions, but these are thrown by Spring Security in the AbstractSecurityInterceptor, as learnt in this response.

Thak you all for your time. Sorry for the post length :)


Solution

  • I started digging in Spring Security libraries, and noticed the session cookie was being set in HttpSessionRequestCache.saveRequest(...) method:

    public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
      if (!this.requestMatcher.matches(request)) {
        if (this.logger.isTraceEnabled()) {
          this.logger.trace(LogMessage.format("Did not save request since it did not match [%s]", this.requestMatcher));
        }
      } else {
        DefaultSavedRequest savedRequest = new DefaultSavedRequest(request, this.portResolver);
        if (!this.createSessionAllowed && request.getSession(false) == null) {
          this.logger.trace("Did not save request since there's no session and createSessionAllowed is false");
        } else {
          request.getSession().setAttribute(this.sessionAttrName, savedRequest);
          if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Saved request %s to session", savedRequest.getRedirectUrl()));
          }
        }
      }
    }
    

    The 'Set-Cookie' header appears when creating the DefaultSavedRequest object. I changed my WebSecurityConfig to the following:

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
      ...
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()
            .and()
              .requestCache().requestCache(getHttpSessionRequestCache()) // This is new
            .and()
              .authorizeRequests().antMatchers(authorizedWithoutAuth).permitAll()
              .anyRequest().authenticated()
            .and()
              .csrf().disable()
            .and()
              .logout().logoutUrl("/api/v1/logout").invalidateHttpSession(true)
              .deleteCookies("mySessionCookie");
      }
    
    
      public HttpSessionRequestCache getHttpSessionRequestCache()
      {
        HttpSessionRequestCache httpSessionRequestCache = new HttpSessionRequestCache();
        httpSessionRequestCache.setCreateSessionAllowed(false); // I modified this parameter 
        return httpSessionRequestCache;
      }
    }
    

    and now it works. When logging in with Authorization headers, the cookie is being set correctly, but all the requests with an invalid session cookie or an expired one return a 401 response without setting a new one.

    Also, after reading this answer, I understood better what this createSessionAllowed was doing