Add secure flag to JSESSIONID cookie in spring automatically - java

I have a tomcat application server that is behind a nginx.
SSL terminates on the nginx.
The Spring web-mvc application that is deployed on the tomcat should set the secure flag on the JSESSIONID.
It would be cool if spring has some automatic detection for this so I don't get bothered during development because I don't have SSL there.
Is there a way to tell spring to set the flag automatically?
I use JavaConfig to setup the application and use Maven to create a deployable war-file.
I have checked this already, but this looks somehow ugly and static:
set 'secure' flag to JSESSION id cookie

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.

If you are using Spring Boot, there is a simple solution for it. Just set the following property in your application.properties:
server.servlet.session.cookie.secure=true
Source: Spring docs - Appendix A. Common application properties
If you have some environment with HTTPS and some without it, you will need to set it to false in profiles without HTTPS. Otherwise the Secure cookie is ignored.

in your application.yml just add
server:
session:
cookie:
secure: true

Behind nginx as ssl terminal point it is not trivial task: secured connection must be detected by nginx header (X-Forwarded-Proto: https, see Using the Forwarded header)
But it is easy solved by nginx config:
if ($scheme = http) {
return 301 https://$http_host$request_uri;
}
proxy_cookie_path / "/; secure";

We have a Spring Boot 2.3 app that uses HTTPS to NGINX and HTTP between NGINX and Tomcat.
Even with this Spring property setting:
server:
servlet:
session:
cookie:
secure: true
... the Secure flag is not being set on the JSESSIONID cookie when the app is accessed via HTTP. You can test this by running the app locally and hitting Tomcat directly using HTTP vs HTTP.
I found that adding this to the config sets the Secure flag for HTTP and HTTPS which fixes our issue when putting NGINX in front of Tomcat using HTTP:
/**
* Fix for GCP... since we use HTTP internally in Kubernetes, Spring will not make JSESSIONID Secure, but this will.
*
* See https://www.javafixing.com/2021/11/fixed-add-secure-flag-to-jsessionid.html
*
* #return
*/
#Bean
public ServletContextInitializer servletContextInitializer() {
return new ServletContextInitializer() {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.getSessionCookieConfig().setSecure(true);
}
};
}

Add another option
You can use a ServletContextInitializer to set secure cookie and http only flag
#Bean
public ServletContextInitializer servletContextInitializer() {
return new ServletContextInitializer() {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.COOKIE));
SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig();
sessionCookieConfig.setHttpOnly(true);
sessionCookieConfig.setSecure(true);
}
};
}

It's working for me
public class WebInitializer implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(AppConfig.class);
ctx.setServletContext(servletContext);
Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
servletContext.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.COOKIE));
SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig();
sessionCookieConfig.setHttpOnly(true);
sessionCookieConfig.setSecure(true);
}
}

Related

How can I get PCF App Manager to display information about my detected as Spring Boot Application?

I have a Spring Boot application that is being deployed to Pivotal Cloud Foundry (PCF).
PCF recognizes the application as a Spring Boot Application. However, the none of the information displays.
The application is running with a context path.
A Configuration has been added to remap:
/cloudfoundryapplication
/actuator/prometheus
/health
To within the context path from without. I can open /actuator/prometheus and /health in my browser.
/cloudfoundryapplication returns an HTTP 401 error page.
I've added
management:
cloudfoundry:
enabled: true
skip-ssl-validation: true
to application.yml.
Spring security is running but
is configured to ignore all of the below URLS.
#Override
public void configure(WebSecurity web) throws Exception
{
web.ignoring()
.antMatchers("/health",
"/actuator/health",
"/actuator/prometheus",
"/cloudfoundryapplication",
"/actuator/cloudfoundryapplication",
"/cloudfoundryapplication/**",
//"/report/daily",
"/index.html",
"/index.html**",
"/favicon.ico");
}
So How can I get PCF to actually display the Health for my application, and treat it like the Spring Boot app it recognizes it to be?
UPDATE:
The application is already serving URLS outside of Its context path. I've already listed the URLS it was serving. Here is the class that does that.
PCF Recognizes the app as Spring Boot, but none of the forms or information is displayed.
#Configuration
public class CloudMetricsConfig
{
#Bean
public TomcatServletWebServerFactory servletWebServerFactory()
{
return new TomcatServletWebServerFactory()
{
#Override
protected void prepareContext(Host host, ServletContextInitializer[] initializers)
{
super.prepareContext(host, initializers);
addContext(host, "/cloudfoundryapplication", getContextPath(),
"cloudfoundry");
addContext(host, "/actuator/prometheus", getContextPath(),
"prometheus");
addContext(host, "/health", getContextPath(),
"health");
}
};
}
private void addContext(Host host, String path, String contextPath,
String servletName)
{
StandardContext child = new StandardContext();
child.addLifecycleListener(new Tomcat.FixContextListener());
child.setPath(path);
ServletContainerInitializer initializer =
getServletContextInitializer(contextPath, servletName, path);
child.addServletContainerInitializer(initializer, Collections.emptySet());
child.setCrossContext(true);
host.addChild(child);
}
private ServletContainerInitializer getServletContextInitializer(String contextPath,
String servletName,
String path)
{
return (c, context) ->
{
Servlet servlet = new GenericServlet()
{
#Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
ServletContext context = req.getServletContext().getContext(contextPath);
context.getRequestDispatcher(path).forward(req, res);
}
};
context.addServlet(servletName, servlet).addMapping("/*");
};
}
}
The three URLS remapped above are set to be ignored in by Spring-Security.
In PCF Apps Manager I can see the following Routes defined for my app.
https://service-dev.apps.nonprod-mpn.ro11.company.com
https://app-dev.platform-test.company.com/exe/v2
https://service-dev.platform-test.company.com
Note: Only the second route has the context-path defined as part of it.
Further Update:
The App Manager is sending a Request to the First listed Route. It doesn't appear to get a response at all.
The Logs look it should be responding though:
2021-01-22T17:03:19.928-05:00 [RTR/9] [OUT] service-dev.apps.nonprod-mpn.ro11.company.com - [2021-01-22T22:03:19.918937681Z] "OPTIONS /exe/v2/cloudfoundryapplication/health HTTP/1.1" 200 0 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15" "10.204.253.14:53711" "10.195.121.117:61118" x_forwarded_for:"10.90.161.66, 10.204.253.14" x_forwarded_proto:"https" vcap_request_id:"0c6b3241-fdea-4e77-4f75-096a673a19a8" response_time:0.008875 gorouter_time:0.003056 app_id:"ea6df148-1809-4f02-87ac-39b4ea0ebeac" app_index:"0" x_cf_routererror:"-" x_b3_traceid:"9afab521ad427253" x_b3_spanid:"9afab521ad427253" x_b3_parentspanid:"-" b3:"9afab521ad427253-9afab521ad427253"
But in Safari it says:
Response
no response headers
That's specifically for the /cloudfoundryapplication/health endpoint.
/cloudfoundryapplication returns a response successfully.
The application is running with a context path.
This requires special handling to make it work. By using a context path the /cloudfoundryapplication endpoint is no longer available. It's /your-context-path/cloudfoundryapplication. This means Apps Manager won't find it.
See...
If a custom servlet context path has been configured, the Spring Boot Actuator endpoints will no longer be at the /cloudfoundryapplication path.
From this KB article.
There is a workaround, which is documented here (as I write this the link in the KB is wrong, but I'll get that fixed).
Quoting:
The configuration will differ depending on the web server in use. For Tomcat, the following configuration can be added:
#Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory() {
#Override
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
super.prepareContext(host, initializers);
StandardContext child = new StandardContext();
child.addLifecycleListener(new Tomcat.FixContextListener());
child.setPath("/cloudfoundryapplication");
ServletContainerInitializer initializer = getServletContextInitializer(getContextPath());
child.addServletContainerInitializer(initializer, Collections.emptySet());
child.setCrossContext(true);
host.addChild(child);
}
};
}
private ServletContainerInitializer getServletContextInitializer(String contextPath) {
return (c, context) -> {
Servlet servlet = new GenericServlet() {
#Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
ServletContext context = req.getServletContext().getContext(contextPath);
context.getRequestDispatcher("/cloudfoundryapplication").forward(req, res);
}
};
context.addServlet("cloudfoundry", servlet).addMapping("/*");
};
}
What this does is to set up a second context in your embedded Tomcat that will look for /cloudfoundryapplication (from the root) and forward those requests to /cloudfoundryapplication under your actual application context (it uses cross context support to do this).
Thus when Apps Manager sends the request to your-https://<host>.apps.example.com/cloudfoundryapplication, it is forwarded to https://<host>.apps.example.com/<context-path>/cloudfoundryapplication and it arrives where it needs to be.
That is only half the battle though.
It's important to understand that for this to work, you still need a route mapped in Cloud Foundry for this traffic. Otherwise, traffic from Apps Manager won't make it to your app. If you are mapping a route with --path, that won't be sufficient as that will only send traffic for that path to your application, and the /cloudfoundryapplication request, which is a different path, will never end up at your application to use the workaround above.
For example, if you've mapped the route https://<host>.apps.example.com/<context-path>, the platform is only going to send traffic for that exact host & path to your app. Since https://<host>.apps.example.com/cloudfoundryapplication is not under that path, traffic to it will never get to your app (it may go to some other app though, so you have to be careful).
To make this workaround function, you'd need to have either a route without a path or two routes, one with your context path and one with /cloudfoundryapplication.
For example:
a.) https://<host>.apps.example.com
b.) https://<host>.apps.example.com/<context-path> + https://<host>.apps.example.com/cloudfoundryapplication.
That in essence means you can only use this workaround with one Spring Boot application per hostname, since all Spring Boot apps will need to receive requests on the same /cloudfoundryapplication path and only one app can be mapped to that route (well technically you can map a route to multiple apps, but that results in traffic split across all the apps round-robin, which isn't what you want here).
One final note, you mentioned...
A Configuration has been added to remap:
I don't see that in the information you posted. You have some Spring Security code, but that is just going to disable Spring Security for those URLs, it's not remapping/redirecting the URLs. If you're remapping URLs elsewhere, then ignore this note. Just wanted to point that out. If you are expecting it to remap/redirect them, it's not going to.

Distinguish requests between multiple connectors/ports in embedded Tomcat in Spring Boot

I have a Spring Boot rest service based application configured on multiple ports that needs to distinguish each request between the port it came through. The idea of having several ports for the application is due to different public and private sub-networks (with different security access levels) that could access different parts of the services exposed by the application.
Conceptually the idea was to add additional connectors to the embedded tomcat and then catch all incoming requests altering them by adding a custom header to each one specifying the "channel" it came through.
The problem I'm facing is that I have no idea how I could catch these incoming requests on a connector level (before it gets to any filter or servlet).
So, for the multi-port solution I have:
#Configuration
public class EmbeddedTomcatConfiguration {
#Value("${server.additional-ports}")
private String additionalPorts;
#Bean
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
Connector[] additionalConnectors = additionalConnector();
if(additionalConnectors != null && additionalConnectors.length > 0) {
tomcat.addAdditionalTomcatConnectors(additionalConnectors);
}
return tomcat;
}
private Connector[] additionalConnector() {
if(StringUtils.isNotBlank(additionalPorts)) {
return Arrays.stream(additionalPorts.split(","))
.map(String::trim)
.map(p -> {
Connector connector = new Connector(Http11NioProtocol.class.getCanonicalName());
connector.setScheme("http");
connector.setPort(Integer.valueOf(p));
return connector;
})
.toArray(Connector[]::new);
}
return null;
}
}
In theory, I could register a custom LifecycleListener to each connector, but, as far as I know, it won't help. I've also heard something about valves, though I'm not sure how to implement them per connector.
Or maybe I'm going a completely wrong way.
I'd really appreciate any help in the matter.
It seems as though you have your heart set on trying a Valve, but, after some more research, I would recommend using a ServletFilter to do this work instead of a Valve.
I believe you can do this work in a Valve, but a Valve must be deployed into the tomcat/lib directory instead of being packaged inside of your application. I would urge you to consider trying to keep your application together in deployable artifact instead of having to remember to deploy one extra jar file to your tomcat instance when creating a new deployment.
From this answer, Difference between getLocalPort() and getServerPort() in servlets you should be able to access the tomcat port by calling getLocalPort() on the HttpServletRequest.
Then based on your idea in the question add a specific context name into the request header.
public class PortContextFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
int localPort = request.getLocalPort();
// if else or case statements
ServletRequest newRequest = PortContextHttpServletRequestWrapper(request, YourPortContext)
filterChain.doFilter(newRequest, response);
}
public void destroy() {
// Nothing to do
}
public void init(FilterConfig filterConfig) throws ServletException {
// Nothing to do.
}
}
After that is done, in theory, you should be able to use the #RequestMapping annotation to route based on the name inside the header.
#PreAuthorize("hasPermission(#your_object, 'YOUR_OBJECT_WRITE')")
#RequestMapping("/yourobject/{identifier}", headers="context=<context_name>", method = RequestMethod.POST)
public String postYourObject(#PathVariable(value = "identifier") YourObject yourObject) {
// Do something here...
...
}
One can also use RequestCondition to route requests based on port, which I think is closer to stock spring. see Spring Boot | How to dynamically add new tomcat connector?

Behind a Reverse Proxy, Spring Security Login Redirects to Wrong Port

I have the following configuration:
A spring-security enabled web application with form-based login
this runs on http port 8080; no https
A reverse proxy running in front
this runs on port 443 and does SSL termination here
the proxy sets appropriate headers (Host, X-Forwarded-Proto)
The issue I'm encountering is that after a successful login, the redirect url that Spring Security builds is https://server:8443/whatever. The url is correct (it's built off of the initial saved request) except for the port. There is nothing running on port 8443 in this configuration.
I see that this is happening in Spring's PortMapperImpl. There are two mappings by default here: 8080 -> 8443 and 80 -> 443.
How do I override PortMapperImpl or implement my own and force Spring to use that? Or is there a different way to solve this problem?
I found a solution. In Spring Java configuration:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${server.port}")
private int serverPort;
#Value("${security.sslRedirectPort}")
private int sslRedirectPort;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.[the usual request matchers, authorizeRequests, etc]
.and()
.requestCache().requestCache(requestCache());
}
private PortMapper portMapper() {
PortMapperImpl portMapper = new PortMapperImpl();
Map<String, String> mappings = Maps.newHashMap();
mappings.put(Integer.toString(serverPort), Integer.toString(sslRedirectPort));
portMapper.setPortMappings(mappings);
return portMapper;
}
private RequestCache requestCache() {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
PortResolverImpl portResolver = new PortResolverImpl();
portResolver.setPortMapper(portMapper());
requestCache.setPortResolver(portResolver);
return requestCache;
}
}
What's happening here:
I'm using the existing server.port setting to inject the http port (by default this is 8080 in spring)
I'm creating a setting called security.sslRedirectPort. Set this property to whatever you want it to redirect to in your application.yaml file.
I create a custom RequestCache and insert it into the spring security configuration. In this custom RequestCache, I set the PortResolver and on it, the PortMapper, and then set the mapping values accordingly.
Now when I run, I get a correct redirect url after logging in, with the port set to whatever I set security.sslRedirectPort to.

How to disable security on management port for Spring Boot app?

I am setting up a Spring Boot 1.3 secured application, but with an management port that is inaccessible to the public, so I don't need any security on this port.
This is simply what I want to achieve:
server.port = 8080 # -> secure
management.port = 8081 # -> unsecure
But as soon as I add a WebSecurityConfigurerAdapter, it automatically is in effect for both ports. Setting management.security.enabled=false has no effect if management port is different, is this a bug? How can I otherwise disable security for the management port only?
My simple security configuration:
#Configuration
#EnableWebSecurity
static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
I know a possible workaround could be to set custom context-path eg. /manage and ignore this path from security, but it doesn't seem ideal to use a non-standard path plus the fiddling to resolve path into the security config without hard-coding it, so I would like to find out whether there is a standard approach to this.
You can always add a request matcher and skip the security checks for the management port. A workaround for Spring Boot 2 is as follows. This might work for older Spring Boot versions too. Please try and see.
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
// this is the port for management endpoints
#Value("${management.server.port}")
private int managementPort;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers(checkPort(managementPort)).permitAll()
.anyRequest().authenticated();
}
/**
* This method verifies whether a request port is equal to the provided method parameter
*
* #param port Port that needs to be checked with the incoming request port
* #return Returns a request matcher object with port comparison
*/
private RequestMatcher checkPort(final int port) {
return (HttpServletRequest request) -> port == request.getLocalPort();
}
}
Inspired by this answer.
Looks like it is in fact a bug: https://github.com/spring-projects/spring-boot/issues/4624

Is there a way to programmatically set SSL as mandatory in Tomcat (e.g. not via web.xml)?

We have a java web application which is deployed in many different clients using Tomcat 7. One of our clients wants us to make SSL mandatory, so that https is enforced. The other deployments should remain with no SSL since they don't have SSL certificates.
We know how to enforce it via security-constraint tags in web.xml, but we'd like to avoid the hassle of having to make a special build to a single client, and would rather be able to set this as a database parameter which would be read by the application and then programmatically enforce SSL or not.
Is this possible? How do we do that?
Thanks a lot in advance.
Jonathas
UPDATE: what we need is that in this specific environment the application automatically redirects por 80 to port 443, disabling port 80 is not a viable option.
You can define a filter that validates wether the connection is secure, via ServletRequest.isSecure. If not secure, and the application in context is one that requires a secure connection, then redirect to your 'https' endpoint. Otherwise, proceed.
Pseudocode:
public class SecureConnectionFilter implements Filter {
private boolean requireSecure;
#Override
public void destroy() {
return;
}
#Override
public void doFilter(final ServletRequest request,
final ServletResponse response, final FilterChain filterChain)
throws IOException, ServletException {
if(requireSecure && ! (request.isSecure())) {
// Redirect to secure endpoint
} else {
filterChain.doFilter(request, response);
}
}
#Override
public void init(FilterConfig arg0) throws ServletException {
// Determine wether the application in context is required
// to be secure or not. If this information is not available
// at startup time then defer this logic to `doFilter`
}
}
If this is just for one client just comment out the plain http connector (and the corresponding port) in server.xml and leave only the secure connector and port available.
Simple solution, does not impact your code or other deployments and your client gets what he wants
Update:
You could redirect to https. Try:
<Connector port="80" protocol="HTTP/1.1"
redirectPort="443"/>
See this also

Categories