Murat Aykanat
Murat Aykanat

Reputation: 1708

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:

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

Upvotes: 1

Views: 275

Answers (2)

Murat Aykanat
Murat Aykanat

Reputation: 1708

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.

Upvotes: 1

so-random-dude
so-random-dude

Reputation: 16555

localhost:8081 (Authentication server) localhost:8083 (UI) localhost:8101 All these will get treated like a different domain by the browser. So a cookie set by one of them will not be sent along with the request to others. You have 2 options here

  1. First option is: If you want to stick with the traditional way of authenticating, let your Auth server handle all the request, let it proxy the UI and Upload service request to the corresponding upstream services. And you can use other means such as firewall - iptables/firewalld or security-groups/ACLs(if you are on cloud) to control access to other 2 apps (UI and Upload)

  2. Second Option is: Modernize your stack, have oauth2 server as the auth server which will issue oauth2/jwt token after authenticating the user. Your javascript (from your webpage running on client's browser) should send that token along with all subsequent requests. Your other services (UI and Upload) will have provisions to talk to Auth server to verify the validity of the token. If found invalid, redirect them to the corresponding page.

Upvotes: 1

Related Questions