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 theOrigin
request header, compare that to a list of allowed origins, and then if theOrigin
value is in the list, to set theAccess-Control-Allow-Origin
value to the same value as theOrigin
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 forblue.acme.com
andred.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 Stephan H Wissel on 07 April 2020 | Comments (3) | categories: Salesforce Singapore