Add Secure Flag to Jsessionid Cookie in Spring Automatically

Add secure flag to JSESSIONID cookie in spring automatically

When you use spring-session, e.g. to persist your session in reddis,
this is indeed done automatically. The cookie is than created by org.springframework.session.web.http.CookieHttpSessionStrategy which in CookieHttpSessionStrategy#createSessionCookie checks if the request comes via HTTPS and sets secure accordingly:

sessionCookie.setSecure(request.isSecure());

If you do not use spring-session, you can configure secure cookies using a ServletContextInitializer.
Use a application property, to set it to true/false depending on a profile.

@Bean
public ServletContextInitializer servletContextInitializer(@Value("${secure.cookie}") boolean secure) {
return new ServletContextInitializer() {

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.getSessionCookieConfig().setSecure(secure);
}
};
}

application.properties (used in dev when profile 'prod' is not active):

secure.cookie=false

application-prod.properties (only used when profile 'prod' is active, overwrites value in application.properties):

secure.cookie=false

start your application on the prod server with :

--spring.profiles.active=prod

Sounds like some effort, if you have not worked with profiles so far, but you will most likely need a profile for prod environment anyway, so its really worth it.

how to set cookies as secure flag in spring boot

In application.properties set the following property:

server.servlet.session.cookie.secure=true

... or in older versions (before ~2018):

server.session.cookie.secure=true

Spring Boot unable to login when set cookie as secured

Update:

The problem is now solved, I think this is because I tried it on my localhost, which I think considered as "unsecured connection" by the browser, and since unsecured connection can't set secure cookie, spring boot apps can't create session, see here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Secure_and_HttpOnly_cookies

When I deployed on my client's server that provides https, it's working. Thank you for all responders.

How to set SameSite and Secure attribute to JSESSIONID cookie


UPDATE on 06/07/2021 - Added correct Path attribute with new sameSite attributes to avoid session cookie duplication with GenericFilterBean approach.


I was able to come up with my own solution for this.

I have two kinds of applications which run on Spring boot which has different Spring security configurations and they needed different solutions to fix this.

CASE 1: No user authentication

Solution 1

In here you might have created an endpoint for the 3rd party response, in your application. You are safe until you access httpSession in a controller method. If you are accessing session in different controller method then send a temporary redirect request to there like follows.

@Controller
public class ThirdPartyResponseController{

@RequestMapping(value=3rd_party_response_URL, method=RequestMethod.POST)
public void thirdPartyresponse(HttpServletRequest request, HttpServletResponse httpServletResponse){
// your logic
// and you can set any data as an session attribute which you want to access over the 2nd controller
request.getSession().setAttribute(<data>)
try {
httpServletResponse.sendRedirect(<redirect_URL>);
} catch (IOException e) {
// handle error
}
}

@RequestMapping(value=redirect_URL, method=RequestMethod.GET)
public String thirdPartyresponse(HttpServletRequest request, HttpServletResponse httpServletResponse, Model model, RedirectAttributes redirectAttributes, HttpSession session){
// your logic
return <to_view>;
}
}

Still, you need to allow the 3rd_party_response_url in your security configuration.

Solution 2

You can try the same GenericFilterBean approach described below.

Case 2: Users need to be authenticated/sign in

In a Spring Web application where you have configured most of your security rules either through HttpSecurity or WebSecurity, check this solution.

Sample security config which I have tested the solution:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.
......
..antMatchers(<3rd_party_response_URL>).permitAll();
.....
..csrf().ignoringAntMatchers(<3rd_party_response_URL>);
}
}

The Important points which I want to highlight in this configuration are you should allow the 3rd party response URL from Spring Security and CSRF protection(if it's enabled).

Then we need to create a HttpServletRequest Filter by extending GenericFilterBean class (Filter class did not work for me) and setting the SameSite Attributes to the JSESSIONID cookie by intercepting each HttpServletRequest and setting the response headers.

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
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.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class SessionCookieFilter extends GenericFilterBean {

private final List<String> PATHS_TO_IGNORE_SETTING_SAMESITE = Arrays.asList("resources", <add other paths you want to exclude>);
private final String SESSION_COOKIE_NAME = "JSESSIONID";
private final String SESSION_PATH_ATTRIBUTE = ";Path=";
private final String ROOT_CONTEXT = "/";
private final String SAME_SITE_ATTRIBUTE_VALUES = ";HttpOnly;Secure;SameSite=None";

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String requestUrl = req.getRequestURL().toString();
boolean isResourceRequest = requestUrl != null ? StringUtils.isNoneBlank(PATHS_TO_IGNORE_SETTING_SAMESITE.stream().filter(s -> requestUrl.contains(s)).findFirst().orElse(null)) : null;
if (!isResourceRequest) {
Cookie[] cookies = ((HttpServletRequest) request).getCookies();
if (cookies != null && cookies.length > 0) {
List<Cookie> cookieList = Arrays.asList(cookies);
Cookie sessionCookie = cookieList.stream().filter(cookie -> SESSION_COOKIE_NAME.equals(cookie.getName())).findFirst().orElse(null);
if (sessionCookie != null) {
String contextPath = request.getServletContext() != null && StringUtils.isNotBlank(request.getServletContext().getContextPath()) ? request.getServletContext().getContextPath() : ROOT_CONTEXT;
resp.setHeader(HttpHeaders.SET_COOKIE, sessionCookie.getName() + "=" + sessionCookie.getValue() + SESSION_PATH_ATTRIBUTE + contextPath + SAME_SITE_ATTRIBUTE_VALUES);
}
}
}
chain.doFilter(request, response);
}
}

Then add this filter to the Spring Security filter chain by

@Override
protected void configure(HttpSecurity http) throws Exception {
http.
....
.addFilterAfter(new SessionCookieFilter(), BasicAuthenticationFilter.class);
}

in order to determine where you need to place the new filter in Spring’s security filter chain, you can debug the Spring security filter chain easily and identify a proper location in the filter chain. Apart from the BasicAuthenticationFilter, after the SecurityContextPersistanceFilter would be an another ideal place.

This SameSite cookie attribute will not support some old browser versions and in that case, check the browser and avoid setting SameSite in incompatible clients.

private static final String _I_PHONE_IOS_12 = "iPhone OS 12_";
private static final String _I_PAD_IOS_12 = "iPad; CPU OS 12_";
private static final String _MAC_OS_10_14 = " OS X 10_14_";
private static final String _VERSION = "Version/";
private static final String _SAFARI = "Safari";
private static final String _EMBED_SAFARI = "(KHTML, like Gecko)";
private static final String _CHROME = "Chrome/";
private static final String _CHROMIUM = "Chromium/";
private static final String _UC_BROWSER = "UCBrowser/";
private static final String _ANDROID = "Android";

/*
* checks SameSite=None;Secure incompatible Browsers
* https://www.chromium.org/updates/same-site/incompatible-clients
*/
public static boolean isSameSiteInCompatibleClient(HttpServletRequest request) {
String userAgent = request.getHeader("user-agent");
if (StringUtils.isNotBlank(userAgent)) {
boolean isIos12 = isIos12(userAgent), isMacOs1014 = isMacOs1014(userAgent), isChromeChromium51To66 = isChromeChromium51To66(userAgent), isUcBrowser = isUcBrowser(userAgent);
//TODO : Added for testing purpose. remove before Prod release.
LOG.info("*********************************************************************************");
LOG.info("is iOS 12 = {}, is MacOs 10.14 = {}, is Chrome 51-66 = {}, is Android UC Browser = {}", isIos12, isMacOs1014, isChromeChromium51To66, isUcBrowser);
LOG.info("*********************************************************************************");
return isIos12 || isMacOs1014 || isChromeChromium51To66 || isUcBrowser;
}
return false;
}

private static boolean isIos12(String userAgent) {
return StringUtils.contains(userAgent, _I_PHONE_IOS_12) || StringUtils.contains(userAgent, _I_PAD_IOS_12);
}

private static boolean isMacOs1014(String userAgent) {
return StringUtils.contains(userAgent, _MAC_OS_10_14)
&& ((StringUtils.contains(userAgent, _VERSION) && StringUtils.contains(userAgent, _SAFARI)) //Safari on MacOS 10.14
|| StringUtils.contains(userAgent, _EMBED_SAFARI)); // Embedded browser on MacOS 10.14
}

private static boolean isChromeChromium51To66(String userAgent) {
boolean isChrome = StringUtils.contains(userAgent, _CHROME), isChromium = StringUtils.contains(userAgent, _CHROMIUM);
if (isChrome || isChromium) {
int version = isChrome ? Integer.valueOf(StringUtils.substringAfter(userAgent, _CHROME).substring(0, 2))
: Integer.valueOf(StringUtils.substringAfter(userAgent, _CHROMIUM).substring(0, 2));
return ((version >= 51) && (version <= 66)); //Chrome or Chromium V51-66
}
return false;
}

private static boolean isUcBrowser(String userAgent) {
if (StringUtils.contains(userAgent, _UC_BROWSER) && StringUtils.contains(userAgent, _ANDROID)) {
String[] version = StringUtils.splitByWholeSeparator(StringUtils.substringAfter(userAgent, _UC_BROWSER).substring(0, 7), ".");
int major = Integer.valueOf(version[0]), minor = Integer.valueOf(version[1]), build = Integer.valueOf(version[2]);
return ((major != 0) && ((major < 12) || (major == 12 && (minor < 13)) || (major == 12 && minor == 13 && (build < 2)))); //UC browser below v12.13.2 in android
}
return false;
}

Add above check in SessionCookieFilter like follows,

if (!isResourceRequest && !UserAgentUtils.isSameSiteInCompatibleClient(req)) {

This filter won't work in localhost environments as it requires a Secured(HTTPS) connection to set Secure cookie attribute.

For a detailed explanation read this blog post.

Forcing Tomcat to use secure JSESSIONID cookie over http

In the end, contrary to my initial tests, web.xml solution worked for me on Tomcat 7.

E.g. I added this snippet to web.xml and it marks session cookie as secure even when reverse proxy contacts tomcat over plain HTTP.

<session-config>
<cookie-config>
<http-only>true</http-only>
<secure>true</secure>
</cookie-config>
</session-config>


Related Topics



Leave a reply



Submit