Caddy Server, CORS, and Preflight Requests

Posted on April 13, 2023  •  4 minutes  • 747 words  • Other languages:  Deutsch

I have been using the Caddy Server lately. I have been loyal to nginx for a long time, but I am thrilled about how easy and fast it is to set up websites with Caddy written in Go. it just saves a lot of boilerplate and work. Caddy takes care of creating certificates (e.g. Lets Encrypt ), automatic redirections from HTTP to HTTPs, supports HTTP3, and just needs a few lines of configuration.

Cross-Site-Scripting, CORS, Preflight-Requests

However, I encountered a problem when I wanted to use Caddy as an API gateway: I ran into cross-origin issues. These problems occur whenever you want to access a foreign domain using JavaScript (e.g. calling api.auxnet.de from auxnet.de). For security reasons, browsers send a so-called preflight request, which is a technically an HTTP request employing the OPTIONS method. The server must respond correctly to this request. Otherwise, an error message will appear in the console. Only if the preflight request has been correctly answered, the browser initiates the actual request (e.g., an API request).

CORS error in Chromium

There are security reasons for this procedure. CORS (Cross-Origin Resource Sharing) is a mechanism designed to prevent Internet resources from being used by unauthorized parties. Details about the procedure can be found, among other places, on the Mozilla Developer Network or in blog posts .

Caddy and CORS

Caddy does a lot automatically, but when it comes to CORS, you have to do it yourself. This is not surprising, Caddy cannot know what settings are necessary in your specific use-case.

We need two components:

Let’s start with the first one. A browser expects a response without content, i.e., 204 No Content. This can be accomplished in Caddy as follows:

api.auxnet.de {
    @cors_preflight {
		method OPTIONS
	}
	respond @cors_preflight 204
}

So we create a named matcher that can respond to requests with the OPTIONS method. Second, we create the directive respond , which, well, responds to the matcher and simply returns 204.

So far, so good. Now we need the so-called CORS headers, of which there seems to be a whole zoo. However, usually only three to four are really relevant, and those should be set in any case:

So let’s expand our hypothetical configuration from above:

api.auxnet.de {
    @cors_preflight {
		method OPTIONS
	}
	respond @cors_preflight 204
	
    header {
        Access-Control-Allow-Origin https://www.auxnet.de
        Access-Control-Allow-Methods GET,POST,OPTIONS,HEAD,PATCH,PUT,DELETE
        Access-Control-Allow-Headers User-Agent,Content-Type,X-Api-Key
        Access-Control-Max-Age 86400
    }
}

We can test our configuration using curl:

curl -I -XOPTIONS https://api.auxnet.de/

The output should contain the following headers (the order is changed because Go orders the keys of the map alphabetically):

Access-Control-Allow-Headers: User-Agent,Content-Type,X-Api-Key
Access-Control-Allow-Methods: GET,POST,OPTIONS,HEAD,PATCH,PUT,DELETE
Access-Control-Allow-Origin: https://www.auxnet.de
Access-Control-Max-Age: 86400

Notice that the headers are sent with every response including GET or POST requests. Browsers seem to require CORS headers for every type of request, not just for the OPTIONS request.

Multiple Origin Domains

Sometimes you might want to allow access from multiple domains. Unfortunately, Access-Control-Allow-Origin allows one domain only, or * for the while Internet. You can use more named matchers to cope with this problem:

api.auxnet.de {
    @cors_preflight {
		method OPTIONS
	}
	respond @cors_preflight 204
	
	@origin1 {
        header Origin https://www.auxnet.de
	}
	header @origin1 {
        Access-Control-Allow-Origin https://www.auxnet.de
	}
	@origin2 {
        header Origin https://auxnet.de
	}
	header @origin2 {
        Access-Control-Allow-Origin https://auxnet.de
	}
	
    header {
        Access-Control-Allow-Methods GET,POST,OPTIONS,HEAD,PATCH,PUT,DELETE
        Access-Control-Allow-Headers User-Agent,Content-Type,X-Api-Key
        Access-Control-Max-Age 86400
    }
}

Here we create the header depending on the origin domain. You can test the setting like this:

curl -I -H'Origin: https://www.auxnet.de'  -XOPTIONS https://api.auxnet.de/
curl -I -H'Origin: https://auxnet.de'  -XOPTIONS https://api.auxnet.de/
curl -I -H'Origin: https://otherdomain.com'  -XOPTIONS https://api.auxnet.de/

The first two requests should return the correct header, the last one should not.

By logging in into comments, two cookies will be set! More information in the imprint.
Follow me