wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

vert.x and CORS


One of the security mechanism for AJAX calls is CORS (Cross-Origin Resource sharing), where a server advice a browser if it can request resources from it, coming from a different domain.

It is then up to the browser to heed that advice. To complicate matters: when the browser wants to POST data (or other similar operations), it will go through a preflight request adding to site latency.

I have to admit, I never fully understood the rationale, since only browsers adhere to CORS, any webserver, Postman or CURL ignore CORS happily.

None, One or All, but not Some

There's another trouble with CORS: The specification only allows for no-access, all-access (using * as value for Access-Control-Allow-Origin, with restrictions) or one specific domain, but not a list of domains.

Mozilla writes

Limiting the possible Access-Control-Allow-Origin values to a set of allowed origins requires code on the server side to check the value of the Origin request header, compare that to a list of allowed origins, and then if the Origin value is in the list, to set the Access-Control-Allow-Origin value to the same value as the Origin value.

For recent work I needed exactly that for vert.x. The solution has a few parts:

  • The configuration to store permitted domains. I choose the config() function for that
  • Decision how to handle requests: I decided to check for the domain ending only, so one entry acme.com would be good enough for blue.acme.com and red.acme.com. Check your use case if that fits
  • The OPTIONS method for the preflight
  • The CORS headers for any request that carries an Origin
  • The router settings to make that work

Router

Router router = Router.router(this.getVertx());
router.route().handler(this::handlerCheckCorsHeaders);
router.route().method(HttpMethod.OPTIONS).handler(this::handlerOptionsMethod);

Handlers

  private void handlerCheckCorsHeaders(final RoutingContext ctx) {
    final HttpServerResponse response = ctx.response();
    final String origin = ctx.request().getHeader("Origin");
    if (origin != null) {
      if (this.isAllowCors(origin)) {
        response.putHeader("Access-Control-Allow-Origin", origin);
        // Tell browser that response might change with origin          
        response.putHeader("Vary", "Origin");
      }
    }
    ctx.next();
  }

  private void handlerOptionsMethod(final RoutingContext ctx) {
    final HttpServerResponse response = ctx.response();
    final String origin = ctx.request().getHeader("Origin");
    if (origin != null) {
      if (this.isAllowCors(origin)) {
        response.putHeader("Access-Control-Allow-Origin", origin);
        response.putHeader("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, HEAD, DELETE");
        // FIXME: what header do we actually need
        response.putHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
      } else {
        // Throw a 400 for people not welcome
        response.setStatusCode(400).end("No CORS, No OPTIONS");
      }
    }
    // Your available methods might vary!
    ctx.end("OPTIONS, GET, POST, PUT, PATCH, HEAD, DELETE");
  }

Cors lookup

/**
   * @param origin - the Host where the request came from
   * @return true if we serve it
   */
  private boolean isAllowCors(final String origin) {
    // Checking the ENDING of the origin, so
    // acme.com will enable a.acme.com, b.acme.com etc
    JsonArray hosts = this.config().getJsonArray("CORS", new JsonArray());
    for (int i = 0; i < hosts.size(); i++ ) {
      if (origin.endsWith(hosts.getString(i))) {
        return true;
      }
    }
    return false;
  }

As usual YMMV


Posted by on 07 April 2020 | Comments (3) | categories: Salesforce Singapore

Comments

  1. posted by Ankush on Sunday 02 August 2020 AD:

    Why not use the vertx-web's built in Cors handler class ?

    Here's a snippet I used recently:

    Set<String> allowedHeaders = new HashSet<>();
    allowedHeaders.add("x-requested-with");
    allowedHeaders.add("Access-Control-Allow-Origin");
    allowedHeaders.add("origin");
    allowedHeaders.add("Content-Type");
    allowedHeaders.add("accept");
    allowedHeaders.add("Authorization");
    
    router.route().handler(CorsHandler
    	.create(Config.getConfig().getString("cors_origins"))
    	.allowCredentials(true)
    	.allowedHeaders(allowedHeaders)
    	.allowedMethod(HttpMethod.POST)
    	.allowedMethod(HttpMethod.GET)
    	.allowedMethod(HttpMethod.PUT)
    	.allowedMethod(HttpMethod.PUT)
    );
    

    Unfortunately, in the latest release it only supports a regex matching of the origins. While multi origin support is released in the beta versions, according to this issue.

    Cheers


  2. posted by Robert Brown III on Tuesday 29 March 2022 AD:

    The code you have published here does not work. I have set up a test application using this code. After running several cross- origin requests from an Angular application, I paired down the code to see why it was continuing to fail. What I discovered was that the route you made is never reached when a request is made cross- origin. Because it never gets called, it never gets the chance to put in the response headers that you mentioned. Apparently, the cross- origin request is blocked internally by Vert.x before the routes are invoked.

    If there is a way around this problem, it is not documented in your blog.


  3. posted by Robert Brown III on Tuesday 29 March 2022 AD:

    Apoloogies:

    Please ignore my previous comment. I made a mistake in configuration of my test, which caused cross- origin requests to be sent from the wrong origin.

    Sorry for the bad comment. I have been working on my application all night and I am half asleep. Now that I have corrected my error I will get some rest...