Felix
Felix

Reputation: 2386

Disable JSESSIONID conditionally in Spring-Boot-Web / Tomcat

I'm trying to disable the creation of the JSESSIONID Cookie conditionally.

I only want to create this cookie if a certain cookie in the request is present.

I'm using spring-boot-starter-web 2.5.3 with spring-boot-starter-security.

What I've tried so far

Spring-Session -> CookieSerializer Bean

I tried to add spring-session and define a custom DefaultCookieSerializer-Bean. It turned out that the JSESSIONID is not actually coming from spring, but from the underlying Tomcat.

Using a javax.servlet.Filter which blocks setting the Set-Cookie Header:

package xxxx.configuration;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

@Configuration
public class CookieConsentConfiguration {

    private static class CookieConsentFilter implements Filter {

        private final ObjectMapper objectMapper;

        private CookieConsentFilter(ObjectMapper objectMapper) {
            this.objectMapper = objectMapper;
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            final HttpServletRequest req = (HttpServletRequest) request;

            if (hasCookieConsent(req)) {
                chain.doFilter(request, response);
            } else {
                chain.doFilter(request, new NoCookiesResponseWrapper((HttpServletResponse) response));
            }
        }

        private boolean hasCookieConsent(HttpServletRequest request) {
            try {
                final Cookie[] cookies = request.getCookies();
                if (cookies != null) {
                    for (Cookie cookie : cookies) {
                        if (cookie.getName().equals("cookie_consent_level")) {
                            final String value = cookie.getValue();
                            final JsonNode json = this.objectMapper.readTree(URLDecoder.decode(value, StandardCharsets.US_ASCII));

                            return json.hasNonNull("strictly-necessary") && json.get("strictly-necessary").booleanValue();
                        } else if (cookie.getName().equals("cookie_consent_user_accepted")) {
                            return Boolean.parseBoolean(cookie.getValue());
                        }
                    }
                }
            } catch (Exception e) {
                return false;
            }

            return false;
        }
    }

    private static class NoCookiesResponseWrapper extends HttpServletResponseWrapper {

        public NoCookiesResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        @Override
        public void addCookie(Cookie cookie) {
            // not allowed
        }

        @Override
        public void setHeader(String name, String value) {
            if (!name.equalsIgnoreCase("set-cookie")) {
                super.setHeader(name, value);
            }
        }

        @Override
        public void addHeader(String name, String value) {
            if (!name.equalsIgnoreCase("set-cookie")) {
                super.addHeader(name, value);
            }
        }
    }

    @Bean
    public Filter cookieConsentFilter(ObjectMapper objectMapper) {
        return new CookieConsentFilter(objectMapper);
    }
}

But this also didn't work. The JSESSIONID is still being created.

Is there any way to disable the creation of this (or any) Cookie if some conditions aren't met?

Upvotes: 3

Views: 2965

Answers (1)

Felix
Felix

Reputation: 2386

Disclaimer: This answer does not directly answer the question, how to stop creating a JSESSIONID based on a condition. I'll not accept this answer for this reason.

GDPR

I found out that Strictly necessary cookies are allowed by default, without a requirement to show the user a cookie-layer before sending the cookie to the client. As I understand, you still have to display the cookie-layer to the user at the very first page impression.

That essentially means, to my understanding, that you are allowed to send Set-Cookie-Headers for Strictly necessary cookies on the users first page load. On that page, the cookie-layer has to be shown, but Strictly necessary cookies may already exists at that point.

For more information, see: https://gdpr.eu/cookies/


Technical stuff

General

First of all, we have to understand under what circumstances a JSESSIONID is created.

It is created whenever some piece of code tries to obtain a session from the application server. That means, if we can structure our code in a way that it doesn't try to obtain a session from the application server, no JSESSIONID-Cookie will be sent to the client.

But there are a few pitfalls. For some things, a session is the most necessary cookie of all:

  • Login: A login (as in human-user-login on a website), strictly requires a session
  • CSRF-Tokens: The server needs something to obtain these Tokens from. It's the session
  • ... more

Example with Spring-Boot

I'm using spring-boot-starter-web in Version 2.5.3. I want a session to be only created if it's truly required, for one of the reasons I mentioned above: The users tries to login or the users accesses a site that requires CSRF Tokens to be created for security reasons.

The first relevant config can be found in my WebSecurityConfigurerAdapter: Setting the SessionCreationPolicy to IF_REQUIRED.

    @Configuration
    public static class GlobalWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        public void configure(WebSecurity web) {
            web.ignoring().antMatchers("/webjars/**", "/css/**", "/js/**");
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf()
                        .and()
                    .headers()
                        .frameOptions()
                        .deny()
                        .and()
                    .sessionManagement()// these two lines
                        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                        .and()
                    .oauth2Login()
                        .and()
                    .logout()
                        .logoutSuccessUrl("/");
        }
    }

The second relevant thing in my case was to only embed the CSRF-Tokens if they're truly required for the page.

I had a couple of thymeleaf templates of which I always used the same <head> Section. In every of my templates, my <head> Section included the following lines:

    <meta name="_csrf" th:content="${_csrf.token}"/>
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>

While this is still required for pages that perform POST, PUT, ... - Requests to my server, it is not required for most of my pages.

I removed the use of the CSRF-Token in every thymeleaf template where it was not required.

Now, a session is only obtained (and a JSESSIONID is created) from the application server once the user tries to access a page that includes a CSRF-Token

Upvotes: 2

Related Questions