Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CSRF issue with Spring + Angular 2 + Oauth2 + CORS


I am developing a client-server application based on Spring 4.3 and Angular (TypeScript) 4.3, in a CORS scenario where, in production, server and client are on different domains. Client ask for REST server APIs via http requests.


1. REST AND OAUTH CONFIGURATION:
The server exposes REST APIs:

@RestController
@RequestMapping("/my-api")
public class MyRestController{

@RequestMapping(value = "/test", method = RequestMethod.POST)   
    public ResponseEntity<Boolean> test()
    {                   
        return new ResponseEntity<Boolean>(true, HttpStatus.OK);            
    }
}     

Protected by Oauth2 as explained on spring documentation. Obviously I modified the above in order to fit my application. Everything works fine: I am able to protect /my-api/test with Oauth2, through refresh_token and access_token. No problems with Oauth2.


2. CORS CONFIGURATION:
Since The server is on a separate domain with respect to the client (server: 10.0.0.143:8080, client: localhost:4200, as I am developing now), I need CORS enabled on server side:

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    ...

    @Autowired 
    SimpleCorsFilter simpleCorsFilter;   

    @Override
    public void configure(HttpSecurity http) throws Exception {

      http
        .cors().and()
        .addFilterAfter(simpleCorsFilter, CorsFilter.class)
        .csrf().disable()  // notice that now csrf is disabled

    ... (the rest of http security configuration follows)...

    }     

}

where SimpleCorsFilter adds the headers I need:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter extends OncePerRequestFilter  {

    public SimpleCorsFilter() {         
    }

    @Override
    public void destroy() {
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        response.addHeader("Access-Control-Allow-Origin", "http://localhost:4200");
        response.addHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,PATCH,OPTIONS");
        response.addHeader("Access-Control-Max-Age", "3600");
        response.addHeader("Access-Control-Allow-Credentials", "true");           
        response.addHeader("Access-Control-Allow-Headers", "MyCustomHeader, Authorization, X-XSRF-TOKEN");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(request, response);
        }

    }
}

Now, If I make an http get or post request with angular2, example:

callPostMethod(tokenData) {

      const url = 'my-api.domain.com/my-api';
      const pars = new HttpParams();
      const body = null;
      let hds = new HttpHeaders()
          .append('Authorization', 'Bearer ' + tokenData.access_token)
          .append('Content-Type', 'application/x-www-form-urlencoded');

      return this.http.post <Installation> (url, body, {
              params : pars,
              headers: hds,
              withCredentials: true
          });
  }

everything works fine. So even CORS configuration seems to be ok.


3. CSRF CONFIGURATION:

If I now enable CSRF Configuration in Spring like this:

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    ...

    @Autowired 
    SimpleCorsFilter simpleCorsFilter;   

    @Override
    public void configure(HttpSecurity http) throws Exception {

      http
        .cors().and()
        .addFilterAfter(simpleCorsFilter, CorsFilter.class)
        .csrf().csrfTokenRepository(getCsrfTokenRepository()) // notice that csrf is now enabled

    ... (the rest of http security configuration follows)...

    }   

    @Bean
    @Autowired
    // used only because I want to setCookiePath to /, otherwise I can simply use
    // http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    public CsrfTokenRepository getCsrfTokenRepository() {
        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        //tokenRepository.setHeaderName("X-XSRF-TOKEN"); // has already this name --> comment
        tokenRepository.setCookiePath("/");
        return tokenRepository;
    }         
}

On the first POST request it gives me 403 error:

"Invalid CSRF Token &#39;null&#39; was found on the request parameter &#39;_csrf&#39; or header &#39;X-XSRF-TOKEN&#39;."


WHY DOES THIS HAPPEN (as far as I understood..) ?
By exploring the mechanism on how CSRF works, I noticed that Spring correctly generates a cookie named XSRF-TOKEN which is set in response cookies (visible by inspecting request with Chrome, Set-Cookie under Response Headers).

What should happen next is that angular, when performing the first POST request, should read the cookie received from Spring and generate a request Header called X-XSRF-TOKEN, whose value is equal to the value of the cookie.

If I check the Header of the failed POST request, I see that there is no X-XSRF-TOKEN, like angular did not made what it should do by its CSRF specification, see the image:

Failed CSRF request, Chrome inspector

Looking at angular implementation of xsrf (/angular/angular/blob/4.3.5/packages/common/http/src/xsrf.ts) you can see that in HttpXsrfInterceptor, csrf headers are not added if the target URL starts with http (what follows is angular xsrf.ts source code copy and paste):

/**
 * `HttpInterceptor` which adds an XSRF token to eligible outgoing requests.
 */
@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {
  constructor(
      private tokenService: HttpXsrfTokenExtractor,
      @Inject(XSRF_HEADER_NAME) private headerName: string) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const lcUrl = req.url.toLowerCase();
    // Skip both non-mutating requests and absolute URLs.
    // Non-mutating requests don't require a token, and absolute URLs require special handling
    // anyway as the cookie set
    // on our origin is not the same as the token expected by another origin.
    if (req.method === 'GET' || req.method === 'HEAD' || lcUrl.startsWith('http://') ||
        lcUrl.startsWith('https://')) {
      return next.handle(req);
    }
    const token = this.tokenService.getToken();

    // Be careful not to overwrite an existing header of the same name.
    if (token !== null && !req.headers.has(this.headerName)) {
      req = req.clone({headers: req.headers.set(this.headerName, token)});
    }
    return next.handle(req);
  }
}


WHAT IF I ADD THE X-XSRF-TOKEN HEADER MYSELF?
Since angular doesn't, I tried to attach the header to the request myself, by modifying request headers as follows:

const hds = new HttpHeaders()
          .append('Authorization', 'Bearer ' + tokenData.access_token)
          .append('Content-Type', 'application/x-www-form-urlencoded');

      if (this.getCookie('XSRF-TOKEN') !== undefined) {
          hds = hds.append('X-XSRF-TOKEN', this.getCookie('XSRF-TOKEN'));
      }

where this.getCookie('XSRF-TOKEN') is a method which reads browser cookies using 'angular2-cookie/services', but this.getCookie('XSRF-TOKEN') returns null.

Why? As far as I understood, javascript cookie retrieval fails because, even if XSRF-TOKEN cookie is returned by Spring in the response, it is not set in the browser because it is in a different domain with respect to the client (server domain: 10.0.0.143:8080, client domain: localhost:4200).

If instead the server runs at localhost too, even on a different port (i.e. server domain: localhost:8080, client domain: localhost:4200), the cookie set from spring server in the response is correctly set in the browser and thus can be retrieved by angular with the method this.getCookie('XSRF-TOKEN').

See what I mean observing results of the two different calls in the following image:

localhost and cross-domain POST requests, chrome inspection

If I am correct, this is coherent with the fact that the domain localhost:4200 cannot read via javascript the cookies of domain 10.0.0.143:8080. Notice that the option withCredentials = true allows cookies to flow from server to client, but only transparently, which means that they are not modifiable via javascript. Only the server can read and write cookies of its own domain. Or even the client can, but only if it runs in the same domain as the server (am I correct?). If instead both server and client run on the same domain, even on different ports, the manual header addition works (but in production server and client are on different domains, so this is not the solution).


SO THE QUESTION IS

At the moment the options are:

  1. If I understood the mechanism correctly, Spring and Angular standard CSRF token exchange mechanism cannot work if client and server are on different domains, because (1) angular implementation does not support it and (2) javascript has no access to the XSRF-TOKEN cookie because the latter is on the domain of the server. If this is the case, can I just rely on stateless oauth2 refresh_token and access_token security, without CSRF? Is it ok from a security perspective?

  2. Or maybe, on the other hand, I am missing something and there is another reason I do not see (that's why I am asking you, dear developers) and actually CSRF and CORS should work, so it is my code which is wrong or is missing something.

Given the scenario, can you tell me what would you do? Is there some error in my code which makes CSRF on cross-domain scenario not work? Please let me know if you need additional information to ask my questions.

Sorry for being a bit long, but I think it was better to explain well the complete solution in order to make you understand the issues I'm facing. And, in addition, the code I wrote and explained can be useful for somebody.

Best regards, Giancarlo

like image 644
gc.mnt Avatar asked Aug 22 '17 11:08

gc.mnt


1 Answers

Ad.2. Your code is perfectly valid and you set everything correctly. CSRF protection in spring is designed with frontend in the same domain as backend. As Angular has no access to CSRF data it obviously can't set it in the modifying requests. And without setting them in server filter in regular headers (not cookie) it is no way to access them.

Ad.1. Security of JWT tokens is good enough as big companies use them successfully. However, remember that token itself should be signed with RSA key (not simpler MAC key) as well as all communication must go through secured connections (https/ssl). Usage of refresh tokens always reduces security slightly. Business application usually omits them. General audience applications have to store them securely and nevertheless have the option to drop they validity in case of abuse.

like image 94
Axinet Avatar answered Oct 07 '22 19:10

Axinet