Search code examples
spring-bootrestspring-securitycross-domaincsrf

Unable to call a secured spring boot rest service from a different origin


I have 3 spring boot applications:

localhost:8081 (Authentication server) localhost:8083 (UI) localhost:8101 (Upload service)

When the users goes to localhost:8083/app it redirects it to localhost:8081/login to present them a form to post their credentials and it redirects users back to localhost:8081/app and displays the website. There is no issue at this point.

However, I wanted to add an upload feature where the user drags/drops some files onto a input with type file like this:

<input type="file" multiple style="height:  100%; width: 100%; z-index: 100; opacity:0" v-bind:name="uploadFieldName" v-bind:disabled="isSaving" v-on:change="filesChange($event.target.name, $event.target.files);">

and it will then call localhost:8101/upload to upload the file via an Axios call.

To avoid CSRF issues I added

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

to the HTML and in JS I have the following to setup Axios to use the token and send the cookie:

var csrfHeader = $("meta[name='_csrf_header']").attr("content");
var csrfToken = $("meta[name='_csrf']").attr("content");
axios.defaults.headers = {  
    'XSRF-TOKEN': csrfToken
}
axios.defaults.withCredentials = true;

My call in JS is as follows:

upload(formData){   
    var url = 'http://localhost:8101/upload';
    return axios.post(url, formData);   
}

In my upload service backend I simply take the files and write their names to test if it works:

@RestController
public class AssetController {    
    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public void importAssets(@RequestParam("upload") MultipartFile[] files){
        for(MultipartFile file: files){            
            System.out.println(file.getOriginalFilename());
        }
    }
}

To allow other domains (the UI application) to access the upload service, I set up the CORS mapping as follows:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**");
    }
}

I also read here that I need to add this for multipart file uploads:

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        insertFilters(servletContext, new MultipartFilter());
    }
}

And finally I have my security configuration as follows:

@EnableOAuth2Sso
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.antMatcher("/**")
                .authorizeRequests()
                .antMatchers("/login**")
                .permitAll()               
                .anyRequest()
                .authenticated()
                .and()
                    .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

    }
}

When I try to upload files I see 2 localhost:8101/upload calls with OPTION status both having the same response and request headers:

Response:

HTTP/1.1 302
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: XSRF-TOKEN=0ef5a80f-4ffc-49ab-bfb0-813ae4ca149e; Path=/
Location: http://localhost:8101/login
Content-Length: 0
Date: Thu, 23 Aug 2018 19:57:08 GMT

Request:

OPTIONS /upload HTTP/1.1
Host: localhost:8101
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://localhost:8083
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Access-Control-Request-Headers: xsrf-token
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,tr;q=0.8

However the POST does not happen. So to test further I permitted /upload**in the SecurityConfig as follows:

@EnableOAuth2Sso
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.antMatcher("/**")
                .authorizeRequests()
                .antMatchers("/login**","/upload**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                    .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

    }
}

With that it started working, I can see an OPTION and a POST in network and I have the following request and response headers:

Request:

POST /upload HTTP/1.1
Host: localhost:8101
Connection: keep-alive
Content-Length: 2507057
Origin: http://localhost:8083
X-XSRF-TOKEN: 4b070c11-9250-40d6-9d2a-d6587e814382
XSRF-TOKEN: 52f8fd81-2c80-493e-b7c6-f0a458b8c2e9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7lG6mO10sPzrDU5l
Accept: */*
Referer: http://localhost:8083/toybox
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,tr;q=0.8
Cookie: JSESSIONID=0EF7E75D8A8187BC7B5FB74341207E76; TSESSION=4A8DE8A4A4A430DF2AC9AF14A0BD0E50; XSRF-TOKEN=4b070c11-9250-40d6-9d2a-d6587e814382

Response:

HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Application-Context: toybox-asset-service:8101
Access-Control-Allow-Origin: http://localhost:8083
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Length: 0
Date: Thu, 23 Aug 2018 20:05:47 GMT

Also in the log I can see the file names posted:

cat-pet-animal-domestic-104827.jpeg
kittens-cat-cat-puppy-rush-45170.jpeg

So the call is successfully made. However, since I want the localhost:8101/upload to be secured so that only authorized users can upload files, permitting it to be freely used by anyone is not an option.

After researching some hours, I come to the conclusion that, somehow, the cookies are not being sent by Axios. Because in the secure /upload scenario I see no cookies being sent in the request.

My questions are:

  • If my theory is correct, how do I send the cookies along with the request?
  • Is there another solution/configuration I can go for?

Any help would be appreciated. Thank you.

Edit 1: I am using oauth2 as the authentication server and I have the following setup in application.properties in both UI and the upload service:

security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
security.oauth2.client.access-token-uri=http://localhost:8081/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:8081/oauth/authorize
security.oauth2.resource.user-info-uri=http://localhost:8081/me

Solution

  • I resolved the issue by using Spring Session. Spring Session uses Redis, and since I am on Windows, I followed the instructions here for running Redis on Windows.

    I downloaded Redis from https://github.com/ServiceStack/redis-windows/raw/master/downloads/redis-latest.zip and unzipped it into C:\Redis. After navigating to the directory via the command prompt, I ran the command redis-server.exe redis.windows.conf (or ./redis-server.exe redis.windows.conf in Powershell)

    I added the following to both UI and the Upload services' POM files

    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    

    And finally I added below to application.properties files of both UI and Upload services:

    spring.session.store-type=redis
    server.servlet.session.timeout=3600
    spring.session.redis.flush-mode=on-save
    spring.session.redis.namespace=spring:session
    
    spring.redis.host=localhost
    spring.redis.password=
    spring.redis.port=6379
    

    Here password is empty because by default Redis does not have a password set, I didn't set one for testing purposes.

    When I restarted my applications, I see that when I login in the UI application, I can successfully call the Upload service using the session I have in the UI application.