Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JHipster: CORS fails on form login (actual request, not pre-flight)

I would like to extend my monolithic setup of jHipster with a second front-end application which accesses the same API from a different URL. As a first step, I've enabled CORS in the application.yml and I'm sending the request from the front-end with the withCredentials flag. I'm using sessions and no JWT authentication.

Many methods work now as expected, but not all. The pre-flight (OPTIONS request) always goes through and works as expected. The response of this call contains the correct CORS headers.

The actual request (e.g. the POST request to sign in), however, requires also a header (Access-Control-Allow-Origin) in the response. This header is automatically set on my custom REST interfaces, but it is not set on jHipster-generated methods like /api/authentication or /api/logout. It does also not work on Spring-Security-protected resources like /api/account (only if not logged in, 401, afterwards it works as expected with the correct headers)

As for the logout, for example, Google Chrome reacts with the following message in the console, even though the call goes through in the Network tab (POST response status 200):

XMLHttpRequest cannot load http://localhost:8080/api/logout. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:9000' is therefore not allowed access.

I was wondering, what I'm doing wrong here. I guess the headers are not properly set. I could now manually add the header (e.g. in the AjaxAuthenticationSuccessHandler), but that does not seem right.

I'm using the rather outdated version of jHipster 3.7.0. I would, however, prefer not to update the core project.

Do you have any idea, what could be causing this issue?



Headers

Here are the complete headers of the POST call to /api/logout. The OPTIONS call works as expected but in the POST response the Access-Control-Allow-Origin header is missing:

OPTIONS Request

OPTIONS /api/logout HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: POST
Origin: http://localhost:9000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Access-Control-Request-Headers: x-csrf-token
Accept: */*
DNT: 1
Referer: http://localhost:9000/
Accept-Encoding: gzip, deflate, br
Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4,de-CH;q=0.2,it;q=0.2

OPTIONS Response

HTTP/1.1 200 OK
Access-Control-Allow-Headers: x-csrf-token
Date: Mon, 11 Sep 2017 13:54:57 GMT
Connection: keep-alive
Access-Control-Allow-Origin: http://localhost:9000
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Length: 0
Access-Control-Allow-Methods: GET,PUT,POST,DELETE,OPTIONS
Access-Control-Max-Age: 1800

POST Request

POST /api/logout HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 0
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://localhost:9000
X-CSRF-TOKEN: [***token removed in this snippet***]
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
DNT: 1
Referer: http://localhost:9000/
Accept-Encoding: gzip, deflate, br
Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4,de-CH;q=0.2,it;q=0.2
Cookie: [***removed cookies in this snippet***]

POST Response

HTTP/1.1 200 OK
Expires: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Set-Cookie: CSRF-TOKEN=null; path=/; Max-Age=0; Expires=Thu, 01-Jan-1970 00:00:00 GMT
Set-Cookie: JSESSIONID=[***removed jsessionID in this snippet***]; path=/; Max-Age=0; Expires=Thu, 01-Jan-1970 00:00:00 GMT
Set-Cookie: remember-me=null; path=/; Max-Age=0; Expires=Thu, 01-Jan-1970 00:00:00 GMT
X-XSS-Protection: 1; mode=block
Pragma: no-cache
Date: Mon, 11 Sep 2017 13:54:57 GMT
Connection: keep-alive
X-Content-Type-Options: nosniff
Content-Length: 0

Reproduction

You can reproduce this behavior by using this demo project of jHipster in version 3.7.0. Then, enable the CORS settings (all of them) in src/main/resources/application.yml. After that, create a new user on localhost:8080 and activate it. Finally, try to authenticate with the following JS snippet from another port (e.g. a simple node server or xampp). You can also try to make a simple POST call to /api/account, which will lead to a 401 error. See the Google Chrome console for the error message.

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script>
<script type="text/javascript">
var Http = axios.create({
  baseURL: 'http://localhost:8080/api',
});

Http.interceptors.request.use(function (config) {
  config.xsrfCookieName = 'CSRF-TOKEN';
  config.xsrfHeaderName = 'X-CSRF-TOKEN';
  config.withCredentials = true;
  return config;
});

var credentials = {
  username: 'test-user',
  password: 'test123',
  rememberMe: true
};

Http.post('authentication', 'j_username=' + credentials.username +
'&j_password=' + credentials.password +
'&remember-me=' + credentials.rememberMe +
'&submit=Login');
</script>
like image 285
ssc-hrep3 Avatar asked Sep 05 '17 20:09

ssc-hrep3


2 Answers

It looks like you are running into this CORS issue where one of the filters before the CorsFilter in Spring Security's filter chain throws an error. The request never reaches the CorsFilter, causing the CORS headers to be missing in the response. That's why Chrome complains about the missing headers in the console even though it's a different error.

You need to put the CorsFilter before CsrfFilter and UsernamePasswordAuthenticationFilter so that if there's an issue with either of those filters, the response still gets the CORS headers. To accomplish this, add the following code to SecurityConfiguration.java:

// import CorsFilter
import org.springframework.web.filter.CorsFilter;
...
...
// inject for JHipster v3 apps, add to constructor for JHipster v4 apps
@Inject
private CorsFilter corsFilter;
...
...
// add this line in the configure method before ".exceptionHandling()"
.addFilterBefore(corsFilter, CsrfFilter.class)

Also in SecurityConfiguration.java, you can set @EnableWebSecurity(debug = true) to see the filter chain for each request. You can verify everything is correct by making sure the CorsFilter is before the CsrfFilter and UsernamePasswordAuthenticationFilter in the chain:

Security filter chain: [
    WebAsyncManagerIntegrationFilter
    SecurityContextPersistenceFilter
    HeaderWriterFilter
    CorsFilter     <--------- Before CsrfFilter and UsernamePasswordAuthenticationFilter
    CsrfFilter
    CsrfCookieGeneratorFilter
    LogoutFilter
    UsernamePasswordAuthenticationFilter
    RequestCacheAwareFilter
    SecurityContextHolderAwareRequestFilter
    RememberMeAuthenticationFilter
    AnonymousAuthenticationFilter
    SessionManagementFilter
    ExceptionTranslationFilter
    FilterSecurityInterceptor
]

If you are running into CSRF issues in your new client, you may need to make a GET request after a POST to refresh the CSRF token. An example of how JHipster handles this can be seen when logging out.

like image 142
Jon Ruddell Avatar answered Oct 28 '22 03:10

Jon Ruddell


Have a look at how the org.springframework.web.cors.CorsConfiguration Spring bean is configured in the JHipster common application properties. I think you are missing the following configuration line:

exposed-headers: "Authorization"

I just documented this a few hours ago, and also added this configuration by default a few hours ago.

In the next release, which should be out tomorrow, you should have a new page called Separating the front-end and the API server that should explain that better - and if you have found a better solution, don't hesitate to improve that page, it's a first version.

like image 40
Julien Dubois Avatar answered Oct 28 '22 02:10

Julien Dubois