CORS implementation

Problem this snippet solves:

Pretty self-explanatory - we had to implement CORS (Cross-Origin-Resource-Sharing) where we had multiple domains, all of which had to be able to make AJAX calls to API's in our 'api.example.com' subdomain. Additionally, we had some partners who also need to be able to call our API's. In some cases, we had to pass cookies in the request.

In the past, various developers had created backend Java code to return the CORS response headers, but almost invariably they did an incomplete job - either returning an invalid value or not returning all the required headers or writing the code such that it wasn't portable across applications. Therefore, I decided to write some 'common' CORS handling code, which would have the benefit of doing 'proper' origin checking and would also immediately return the OPTIONS preflight response directly from F5, thus improving performance.

After much hacking around, here is what I came up with.

We used a class to define multiple top-level domains as 'allowed' origins - this would contain both your domains and also those of any partners whom you want to allow to make CORS requests to your site.

If you just have multiple subdomains on a single domain (e.g. www.example.com, api.example.com, code.example.com), you could simply use [HTTP::header Origin] ends_with ".example.com" - it's a little simpler.

I'm always returning the Access-Control-Allow-Credentials: true response header for 'valid' non-OPTIONS (e.g. GET/POST) CORS requests, even if it's not required (i.e. even if the withCredentials flag was not set in the request) - unfortunately, there is no way to know whether it is needed simply by looking at the request, so it's the only way to ensure client errors don't occur.

I'm passing the value of the Access-Control-Request-Method request header in the Access-Control-Allow-Methods response header (e.g. a single value of 'GET' or 'POST' or whatever) - in most implementations, you'll see people returning somethign like a string like Access-Control-Allow-Methods: GET, POST, PUT, but there's really no significant benefit to doing this - much simpler to only return what is passed. In either case, it will be cached by the browser because of the Access-Control-Max-Age response header.

Note that because you will be returning a specific Access-Control-Allow-Origin value, rather than '*', you should also return the Vary: Origin response header. This may have issues with browser caching or if you use a CDN like Akamai or Cloudflare - you should consult any CDN product documentation. There are multiple good sources for explaining the Vary header - Google is your friend.

If anyone has any comments, please add them, good or bad! I would love to know if someone finds this snippet useful...

Code :

# Domains that are allowed to make cross-domain calls to example.com
class allowed_origins {
    ".example.com"
    ".example2.com"
    ".goodpartner.com"
}

when HTTP_REQUEST {
    unset -nocomplain cors_origin
    if { [class match [HTTP::header Origin] ends_with allowed_origins] } {
        if { ( [HTTP::method] equals "OPTIONS" ) and ( [HTTP::header exists "Access-Control-Request-Method"] ) } {
            # CORS preflight request - return response immediately
            HTTP::respond 200 "Access-Control-Allow-Origin" [HTTP::header "Origin"] \
                              "Access-Control-Allow-Methods" [HTTP::header "Access-Control-Request-Method"] \
                              "Access-Control-Allow-Headers" [HTTP::header "Access-Control-Request-Headers"] \
                              "Access-Control-Max-Age" "86400" \
                              "Vary" "Origin"
        } else {
            # CORS GET/POST requests - set cors_origin variable
            set cors_origin [HTTP::header "Origin"]
        }
    }
}

when HTTP_RESPONSE {
    # CORS GET/POST response - check cors_origin variable set in request
    if { [info exists cors_origin] } {
        HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
        HTTP::header insert "Access-Control-Allow-Credentials" "true"
        HTTP::header insert "Vary" "Origin"
    }
}

Tested this on version:

11.0
Published Nov 03, 2015
Version 1.0