I have a Spring Boot Web Application (Spring boot version 2.0.3.RELEASE) and running in an Apache Tomcat 8.5.5 server.
With the recent security policy which has imposed by Google Chrome (Rolled out since 80.0), it is requested to apply the new SameSite attribute to make the Cross-site cookie access in a more secure way instead of the CSRF. As I have done nothing related that and Chrome has set default value SameSite=Lax for the first-party cookies, one of my third-party service integration is failing due to the reason that chrome is restricting access of cross-site cookies when SameSite=Lax and if the third party response is coming from a POST request (Once the procedure completes third-party service redirect to our site with a POST request). in there Tomcat unable to find the session so it appends a new JSESSIONID (with a new session and the previous session was killed) at the end of the URL. So Spring rejects the URL as it contains a semicolon which was introduced by the new JSESSIONID append.
So I need to change the JSESSIONID cookie attributes(SameSite=None; Secure) and tried it in several ways including WebFilters.I have seen the same question and answers in Stackoverflow and tried most of them but ended up in nowhere.
can someone come up with a solution to change those headers in Spring Boot, please?
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.
I was in same situation earlier. Since there is nothing like SameSite in javax.servlet.http.Cookie class so it's not possible to add that.
Part 1: So what I did is wrote a filter which intercepts the required third party request only.
public class CustomFilter implements Filter {
private static final String THIRD_PARTY_URI = "/third/party/uri";
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if(THIRD_PARTY_URI.equals(request.getRequestURI())) {
chain.doFilter(request, new CustomHttpServletResponseWrapper(response));
} else {
chain.doFilter(request, response);
}
}
enter code here
// ... init destroy methods here
}
Part 2: Cookies are sent as Set-Cookie response header. So this CustomHttpServletResponseWrapper overrides the addCookie method and check, if it is the required cookie (JSESSIONID), instead of adding it to cookie, it adds directly to response header Set-Cookie with SameSite=None attribute.
public class CustomHttpServletResponseWrapper extends HttpServletResponseWrapper {
public CustomHttpServletResponseWrapper(HttpServletResponse response) {
super(response);
}
#Override
public void addCookie(Cookie cookie) {
if ("JSESSIONID".equals(cookie.getName())) {
super.addHeader("Set-Cookie", getCookieValue(cookie));
} else {
super.addCookie(cookie);
}
}
private String getCookieValue(Cookie cookie) {
StringBuilder builder = new StringBuilder();
builder.append(cookie.getName()).append('=').append(cookie.getValue());
builder.append(";Path=").append(cookie.getPath());
if (cookie.isHttpOnly()) {
builder.append(";HttpOnly");
}
if (cookie.getSecure()) {
builder.append(";Secure");
}
// here you can append other attributes like domain / max-age etc.
builder.append(";SameSite=None");
return builder.toString();
}
}
As mentioned in this answer:
Same-Site flag for session cookie in Spring Security
#Configuration
public static class WebConfig implements WebMvcConfigurer {
#Bean
public TomcatContextCustomizer sameSiteCookiesConfig() {
return context -> {
final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
cookieProcessor.setSameSiteCookies(SameSiteCookies.NONE.getValue());
context.setCookieProcessor(cookieProcessor);
};
}
}
but this seems even simpler
#Configuration
public static class WebConfig implements WebMvcConfigurer {
#Bean
public CookieSameSiteSupplier cookieSameSiteSupplier(){
return CookieSameSiteSupplier.ofNone();
}
}
Or ... even simpler, spring boot since 2.6.0 supports setting it in application.properties.
Spring documentation about SameSite Cookies
server.servlet.session.cookie.same-site = none
Related
An spring boot application is hosted behind 2 reverse proxy (chained).
reverse-proxy 1 --> reverse-proxy 2 --> spring boot app
And the host and forward headers are not chain correctly. there is a way to force the host to a fixed value? like the hostname of the "reverse proxy 1"?
i have fixed my issue by changing the serverName in incoming request.
i have add a valve to tomcat:
public class HostForceValve extends ValveBase {
private final String proxyName;
public HostForceValve(String proxyName) {
this.proxyName = proxyName;
}
#Override public void invoke(Request request, Response response) throws IOException, ServletException {
org.apache.coyote.Request coyoteRequest = request.getCoyoteRequest();
MimeHeaders mimeHeaders = coyoteRequest.getMimeHeaders();
mimeHeaders.removeHeader("host");
final MessageBytes host = mimeHeaders.addValue("host");
host.setString(proxyName);
request.setRemoteHost(proxyName);
request.getCoyoteRequest().serverName().setString(proxyName);
try {
Valve next = getNext();
if (null == next) {
return;
}
next.invoke(request, response);
} finally {
request.setRemoteHost(proxyName);
}
}
}
And add this value to the tomcat embedded server:
#Component
public class MyTomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Value("${proxyName:}")
private String proxyName;
#Override
public void customize(TomcatServletWebServerFactory factory) {
final Collection<Valve> currents = factory.getEngineValves();
final ArrayList<Valve> addValves = new ArrayList<>(currents);
if (StringUtils.hasLength(proxyName)) {
addValves.add(0, new HostForceValve(proxyName));
}
factory.setEngineValves(addValves);
}
}
I am trying to redirect http to https in my spring boot application using:
http.requiresChannel().anyRequest().requiresSecure();
But I am getting ERR_TOO_MANY_REDIRECTS. The reason for this is that the load balancer converts all the https to http and directs the http to port 8082, therefore the app never seems to see the https.
I tried to fix this by adding isSecure before the http to https redirection, like this in my configuration:
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
//variables
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/css/**", "/js/**", "/admin/**")
.permitAll().anyRequest().authenticated().and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class)
.formLogin().loginPage("/login").permitAll().and()
.logout().logoutSuccessUrl("/");
//hsts
http.headers().httpStrictTransportSecurity()
.includeSubDomains(true).maxAgeInSeconds(31536000);
http.addFilterBefore(new IsSecureFilter(), ChannelProcessingFilter.class);
//https compulsion
if(!isSecureFilter.isSecure()) {
http.requiresChannel().anyRequest().requiresSecure();
}
}
//rest of the code
}
I am trying to use HttpServletRequestWrapper so that I can repeatedly use isSecure in WebSecurityConfiguration above through the IsSecureFilter I have created below, to prevent infinite redirects:
public class RequestWrapper extends HttpServletRequestWrapper {
private boolean isSecure;
public RequestWrapper(HttpServletRequest request) throws IOException
{
//So that other request method behave just like before
super(request);
this.isSecure = request.isSecure();
}
//Use this method to read the request isSecure N times
public boolean isSecure() {
return this.isSecure;
}
}
Below is the filter that I am trying to inject in WebSecurityConfiguration, to use it's isSecure value above :
#Component
public class IsSecureFilter extends GenericFilterBean {
private boolean isSecure;
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = new RequestWrapper((HttpServletRequest) request);
this.isSecure = req.isSecure();
chain.doFilter(req, response);
}
public boolean isSecure() {
return this.isSecure;
}
}
So running the above code and putting example.com/login in the browser does redirect to https://example.com/login, but i am still getting ERR_TOO_MANY_REDIRECTS.
I can't understand what I am doing wrong?
My first thoughts are:
Can I inject the IsSecureFilter in WebSecurityConfiguration to retrieve isSecure?
Am I adding the IsSecureFilter filter in a correct way to the configuration.
Is the wrapper filter relationship defined correctly?
EDIT
1) I changed http.addFilterAfter(new isSecureFilter(), ChannelProcessingFilter.class); to http.addFilterAfter(isSecureFilter, ChannelProcessingFilter.class);, still no effect.
2) I tried changing http.addFilterBefore(isSecureFilter, ChannelProcessingFilter.class); to http.addFilterAfter(isSecureFilter, ChannelProcessingFilter.class); but that still did not change anything.
Here is the solution to resolve this issue. Based on investigation, since 8080 and 8082 are used to identify HTTP traffic and HTTPS traffic, some code are added to check the port number instead "isSecure" to decide whether redirect HTTP request or not. The code is like following:
public class IsSecureFilter extends GenericFilterBean {
private boolean isSecure;
private int port;
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = new RequestWrapper((HttpServletRequest) request);
HttpServletResponse res = (HttpServletResponse) response;
this.isSecure = req.isSecure();
this.port = req.getLocalPort();
System.out.println("[DEBUG] : isSecure FILTER :: " + isSecure);
System.out.println("[DEBUG] : port FILTER :: " + port);
System.out.println("[DEBUG] : URL :: " + req.getRequestURL());
String url = req.getRequestURL().toString().toLowerCase();
if(url.endsWith("/login") && url.startsWith("http:") && port == 8080){
url = url.replace("http:", "https:");
String queries = req.getQueryString();
if (queries == null) {
queries = "";
} else {
queries = "?" + queries;
}
url += queries;
res.sendRedirect(url);
}
else {
chain.doFilter(req, response);
}
}
public boolean isSecure() {
return this.isSecure;
}
public boolean setIsSecure(boolean isSecure) {
return this.isSecure = isSecure;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
and remove http.requiresChannel().anyRequest().requiresSecure() in WebSecurityConfiguration class.
I have a Spring Boot app using CAS WebSecurity to make sure that all incoming non authenticated requests are redirected to a common login page.
#Configuration
#EnableWebSecurity
public class CASWebSecurityConfig extends WebSecurityConfigurerAdapter {
I want to expose health endpoints through actuator, and added the relevant dependency. I want to bypass the CAS check for these /health URL which are going to be used by monitoring tools, so in the configure method, I have added :
http.authorizeRequests().antMatchers("/health/**").permitAll();
This works, but now I want to tweak it further :
detailed health status (ie "full content" as per the docs) should be accessible only to some specific monitoring user, for which credentials are provided in property file.
if no authentication is provided, then "status only" should be returned.
Following http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-monitoring.html#production-ready-health-access-restrictions, I've configured the properties as below, so that it should work :
management.security.enabled: true
endpoints.health.sensitive: false
But I have a problem with how I configure the credentials... following http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-monitoring.html#production-ready-sensitive-endpoints , I added in my config file :
security.user.name: admin
security.user.password: secret
But it's not working - and when I don't put the properties, I don't see the password generated in logs.
So I'm trying to put some custom properties like
healthcheck.username: healthCheckMonitoring
healthcheck.password: healthPassword
and inject these into my Security config so that configureGlobal method becomes :
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth,
CasAuthenticationProvider authenticationProvider) throws Exception {
auth.inMemoryAuthentication().withUser(healthcheckUsername).password(healthcheckPassword).roles("ADMIN");
auth.authenticationProvider(authenticationProvider);
}
and in the configure method, I change the config for the URL pattern to :
http.authorizeRequests()
.antMatchers("/health/**").hasAnyRole("ADMIN")
.and().httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable();
With that config, I get full content when authenticated, but logically, I don't get any status (UP or DOWN) when I'm not authenticated, because the request doesn't even reach the endpoint : it is intercepted and rejected by the security config.
How can I tweak my Spring Security config so that this works properly ? I have the feeling I should somehow chain the configs, with the CAS config first allowing the request to go through purely based on the URL, so that the request then hits a second config that will do basic http authentication if credentials are provided, or let the request hit the endpoint unauthenticated otherwise, so that I get the "status only" result.. But at the same time, I'm thinking Spring Boot can manage this correctly if I configure it properly..
Thanks !
Solution is not great, but so far, that's what works for me :
in my config (only the relevant code):
#Configuration
#EnableWebSecurity
public class CASWebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
//disable HTTP Session management
http
.securityContext()
.securityContextRepository(new NullSecurityContextRepository())
.and()
.sessionManagement().disable();
http.requestCache().requestCache(new NullRequestCache());
//no security checks for health checks
http.authorizeRequests().antMatchers("/health/**").permitAll();
http.csrf().disable();
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
http // login configuration
.addFilter(authenticationFilter())
.authorizeRequests().anyRequest().authenticated();
}
}
Then I added a specific filter :
#Component
public class HealthcheckSimpleStatusFilter extends GenericFilterBean {
private final String AUTHORIZATION_HEADER_NAME="Authorization";
private final String URL_PATH = "/health";
#Value("${healthcheck.username}")
private String username;
#Value("${healthcheck.password}")
private String password;
private String healthcheckRole="ADMIN";
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = this.getAsHttpRequest(request);
//doing it only for /health endpoint.
if(URL_PATH.equals(httpRequest.getServletPath())) {
String authHeader = httpRequest.getHeader(AUTHORIZATION_HEADER_NAME);
if (authHeader != null && authHeader.startsWith("Basic ")) {
String[] tokens = extractAndDecodeHeader(authHeader);
if (tokens != null && tokens.length == 2 && username.equals(tokens[0]) && password.equals(tokens[1])) {
createUserContext(username, password, healthcheckRole, httpRequest);
} else {
throw new BadCredentialsException("Invalid credentials");
}
}
}
chain.doFilter(request, response);
}
/**
* setting the authenticated user in Spring context so that {#link HealthMvcEndpoint} knows later on that this is an authorized user
* #param username
* #param password
* #param role
* #param httpRequest
*/
private void createUserContext(String username, String password, String role,HttpServletRequest httpRequest) {
List<GrantedAuthority> authoritiesForAnonymous = new ArrayList<>();
authoritiesForAnonymous.add(new SimpleGrantedAuthority("ROLE_" + role));
UserDetails userDetails = new User(username, password, authoritiesForAnonymous);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private HttpServletRequest getAsHttpRequest(ServletRequest request) throws ServletException {
if (!(request instanceof HttpServletRequest)) {
throw new ServletException("Expecting an HTTP request");
}
return (HttpServletRequest) request;
}
private String[] extractAndDecodeHeader(String header) throws IOException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
} catch (IllegalArgumentException var7) {
throw new BadCredentialsException("Failed to decode basic authentication token",var7);
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if(delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
} else {
return new String[]{token.substring(0, delim), token.substring(delim + 1)};
}
}
}
Every time I make a PUT Ajax call to my service, it return the following error:
XMLHttpRequest cannot load http://localhost:8080/users/edit. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access. The response had HTTP status code 403.
After 2 days of investigation, I've reached to try the next solution on my code.
This is the main class where I load the necessary classes and run the application:
#SpringBootApplication
#EnableAutoConfiguration
public class Application extends SpringBootServletInitializer{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DispatcherServletInitializer.class, OptionsController.class,Application.class);
}
}
The DispatcherServilet initializer, where I enable the dispatchOptionsRequest:
public abstract class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("dispatchOptionsRequest", "true");
super.customizeRegistration(registration);
}
}
A controller for handle all OPTIONS request:
#Controller
public class OptionsController {
#RequestMapping(method = RequestMethod.OPTIONS)
public HttpServletResponse handle(HttpServletResponse theHttpServletResponse) throws IOException {
theHttpServletResponse.addHeader("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with");
theHttpServletResponse.addHeader("Access-Control-Max-Age", "60");
theHttpServletResponse.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
theHttpServletResponse.addHeader("Access-Control-Allow-Origin", "*");
return theHttpServletResponse;
}
}
What I'm doing wrong with the configuration?
Finally, the DispatcheServlet customize initializer was the class that really solved my problem. The OPTIONS request was failing because of the optionsController I had implemented, it was wrong.
So I removed that optionsController, and just by adding the handle method in my Rest Controller for the OPTIONS request, the problem was solved:
#CrossOrigin(origins = "*", maxAge = 3600)
#RestController
#RequestMapping("/users")
public class Users {
#RequestMapping(
value = "/edit",
method = RequestMethod.PUT)
public ResponseEntity<?> create(#RequestBody User user){
....
....
}
#RequestMapping(
value = "/**",
method = RequestMethod.OPTIONS
)
public ResponseEntity handle() {
return new ResponseEntity(HttpStatus.OK);
}
}
If you use a modern version of Spring (4.2) you can benefit of the #CrossOrigin.
Indeed if you use Spring < 4.2v you can create a Servlet Filter and put hear the header for CORS support like below:
package it.valeriovaudi.web.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
Copyright 2015 Valerio Vaudi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
public class CORSFilter implements Filter {
public static final String ACCESS_CONTROL_ALLOW_ORIGIN_NAME = "Access-Control-Allow-Origin";
public static final String DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE = "*";
public static final String ACCESS_CONTROL_ALLOW_METHDOS_NAME = "Access-Control-Allow-Methods";
public static final String DEFAULT_ACCESS_CONTROL_ALLOW_METHDOS_VALUE = "POST, GET, OPTIONS, DELETE";
public static final String ACCESS_CONTROL_MAX_AGE_NAME = "Access-Control-Max-Age";
public static final String DEFAULT_ACCESS_CONTROL_MAX_AGE_VALUE = "3600";
public static final String ACCESS_CONTROL_ALLOW_HEADERS_NAME = "Access-Control-Allow-Headers";
public static final String DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS_VALUE = "x-requested-with";
private String accessControlAllowOrigin = DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE;
private String accessControlAllowMethods = DEFAULT_ACCESS_CONTROL_ALLOW_METHDOS_VALUE;
private String accessControlAllowMaxAge = DEFAULT_ACCESS_CONTROL_MAX_AGE_VALUE;
private String accessControlAllowHeaders = DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS_VALUE;
/**
* #return the method return a map that associated the name of paramiters in the web.xml to the class variable name for the header binding*/
private Map<String,String> initConfig(){
Map<String, String> result = new HashMap<>();
result.put(ACCESS_CONTROL_ALLOW_ORIGIN_NAME,"accessControlAllowOrigin");
result.put(ACCESS_CONTROL_ALLOW_METHDOS_NAME,"accessControlAllowMethods");
result.put(ACCESS_CONTROL_MAX_AGE_NAME,"accessControlAllowMaxAge");
result.put(ACCESS_CONTROL_ALLOW_HEADERS_NAME,"accessControlAllowHeaders");
return result;
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
String initParameterValue;
Map<String, String> stringStringMap = initConfig();
for (Map.Entry<String, String> stringStringEntry : stringStringMap.entrySet()) {
initParameterValue = filterConfig.getInitParameter(stringStringEntry.getKey());
// if the init paramiter value isn't null then set the value in the correct http header
if(initParameterValue!=null){
try {
getClass().getDeclaredField(stringStringEntry.getValue()).set(this, initParameterValue);
} catch (IllegalAccessException | NoSuchFieldException ignored) { }
}
}
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_NAME, accessControlAllowOrigin);
response.setHeader(ACCESS_CONTROL_ALLOW_METHDOS_NAME, accessControlAllowMethods);
response.setHeader(ACCESS_CONTROL_MAX_AGE_NAME, accessControlAllowMaxAge);
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_NAME, accessControlAllowHeaders);
filterChain.doFilter(servletRequest, servletResponse);
}
#Override
public void destroy() {
}
}
in Spring boot you can register this filter as spring bean and Spring will register the filter for you.
I hope that this can help you.
I am using Spring 4 to create an API and I need to have authentication for the API requests.
Currently, I have created a HandlerInterceptorAdapter to pick out authentication related headers and perform some validation on those values.
If everything is OK, I set the SecurityContext to a custom implementation of Authentication then in the postHandle I set the authentication to null.
Everything works great, except I keep getting warnings in Tomcat7 about ThreadLocal variables not being removed when the application shuts down.
SEVERE: The web application [] created a ThreadLocal with key of type [java.lang.ThreadLocal] (value [java.lang.ThreadLocal#6c3e4fdb]) and a value of type [org.springframework.security.core.context.SecurityContextImpl] (value [org.springframework.security.core.context.SecurityContextImpl#ffffffff: Null authentication]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
I get that I may be doing this totally wrong, if so I would love some direction. :D
Here is my interceptor:
/**
* Intercepts Requests to set the Authentication in the SecurityContext.
* Sets the response to 401 - Unauthorized, if the header is missing
*/
#Component
public class AuthenticationHandlerInterceptor extends HandlerInterceptorAdapter {
private HandlerMediator mediator;
Logger log = LoggerFactory.getLogger(AuthenticationHandlerInterceptor.class);
#Autowired
public AuthenticationHandlerInterceptor(HandlerMediator mediator) {
this.mediator = mediator;
}
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String username = request.getHeader("authentication-username");
String token = request.getHeader("authentication-token");
// if the remote host is local, then override the authentication
if (request.getRemoteHost().equals("0:0:0:0:0:0:0:1") || request.getRemoteHost().equals("127.0.0.1")) {
log.info("On localhost, overriding authentication with localhost");
username = "TEST";
token = "localhost:localhost";
}
if (username == null || username.trim().length() == 0) {
failAuthentication(response, "Missing Authentication Username Header");
return false;
}
if (token == null || token.trim().length() == 0 || !token.contains(":")) {
failAuthentication(response, "Missing Authentication Token Header");
return false;
}
String[] keys = token.split(":");
String appName = keys[0];
String apikey = keys[1];
if (!appName.equals("localhost")) {
// we are not under localhost so we have to authenticate the application calling us
if (mediator.executeCommand(new AuthenticateApplicationCommand(appName, apikey)) == false) {
failAuthentication(response, "Application Token failed authentication");
return false;
}
}
SecurityContextHolder.getContext().setAuthentication(new ApiAuthentication(username, appName));
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
SecurityContextHolder.getContext().setAuthentication(null);
}
private void failAuthentication(HttpServletResponse response, String message) throws Exception {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("text/html;charset=UTF-8");
ServletOutputStream out = response.getOutputStream();
out.println(message);
out.close();
}
}
How do I get rid of these warnings?
Thanks,
Joe
To do authentication based on the content of HTTP headers, the framework foresees a defining custom authentication filter, plugged in the spring security chain via configuration similar to this (see also this answer):
<security:http>
...
<security:custom-filter ref="customAuthenticationFilter" after="SECURITY_CONTEXT_FILTER" />
</security:http>
There in this filter it's possible to add code similar to this (see also BasicAuthenticationFilter):
try {
....
SecurityContextHolder.getContext().setAuthentication(authResult);
} catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
}
Have a look at class ThreadLocalSecurityContextHolderStrategy,it's there that the SecurityContextImpl instance is getting stored in a ThreadLocal and cleared.
You could try to call clearContext() in AuthenticationHandlerInterceptor, but it's probably better to use the hook the framework foresees as that should lead to less surprises similar to the one you reported.