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

Was this article helpful?

20 Comments

  • You can put this class definition AFTER the "when HTTP_REQUEST {" and it should be fine

     

  • Mr. Jeremy Desca, you need to make the modification that Mr. DrLemongelo has to write in his comment, you need to abandon the class use, and after that your irule will be not have more syntax issues, trust me I have tested.

     

    Kind regards.

     

  • Tom_L's avatar
    Tom_L
    Icon for Nimbostratus rankNimbostratus

    I stopped trying to use class, and tried to utilize the Data group object with the recommended class match brackets, but keep getting the error below when I try to Update/Save the iRule.

     

    01070151:3: Rule [/Common/CORS-Response-Header-iRule] error: /Common/CORS-Response-Header-iRule:1: error: [undefined procedure: ltm][ltm data-group internal DG-CORS-ALLOWED-ORIGINS {

    records {

    .XXXXXXXX.com { }

    localhost:443 { }

    }

    type string

    }]

     

    Version BIG-IP 14.1.2.3 Build 0.0.5 Release 3

  • Tom_L's avatar
    Tom_L
    Icon for Nimbostratus rankNimbostratus

    Disregard my last post. This is now resolved. This was an issue on my end with defining the Data Group.

     

  • Hi Tom,

     

    Can you post your datagroup and Irule as an example please ?

     

    Thank you very much.

  • You should create a data group called "allowed_origins" type string and add the list of your domains to it. You can then make a call to this data group from an iRule.

     

  • madel's avatar
    madel
    Icon for Nimbostratus rankNimbostratus

    Kindly What do you mean about create data group ?

  • madel Go to Local Traffic ›› iRules : Data Group List and click the "Create" button at the far right side of the display. Name the data group "allowed_origins" and set the type to String in the drop-down box. Enter each domain or subdomain (e.g., ".mycompany.com" or ".other.company.org" without the quotes) in the String entry field and click the "Add" button -- there is no need to enter data in the Value entry field. Once you've completed entering data, click the Finished button.