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 (1) | 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