Unable to call a secured spring boot rest service from a different origin
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.
localhost:8083/app
localhost:8081/login
localhost:8081/app
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.
localhost:8101/upload
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:
/upload**
SecurityConfig
@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.
localhost:8101/upload
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.
/upload
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
2 Answers
2
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
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)
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.
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)
C:Redis
redis-server.exe redis.windows.conf
./redis-server.exe redis.windows.conf
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:
application.properties
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.
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
Actually I was already using oauth2 server as authentication server. UI service authenticates successfully but the upload service does not for some reason. I added the details to the question.
– Murat Aykanat
Aug 24 at 8:56