Secure an API with Access-Control-Allow-Headers

Share this video with your friends

Send Tweet
Published a year ago
Updated a year ago

The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request.

In this lesson, we'll secure our API by building an allowlist of origins that can access our API, and update the CORS functionality in our code to adopt that allowlist when handling requests.

Kristian Freeman: [0:00] Thinking about where you deploy your frontend UI is an important step when you're thinking about how your serverless API should work. Back in the CORS headers configuration of our serverless API, we setup this Access-Control-Allow-Origin. We set it to this asterisk, which is known as a wildcard string.

[0:18] This is basically saying that our serverless API can be accessed by any origin. Whether it's my website or your website, anyone can access this. That's fine for some APIs, but in many cases, you might want to make this more specific. You may want to allow it to be accessed by, say, my UI or my local setup.

[0:38] If we come back to Pages, we see that the serverless-api-viewer has deployed. We can also see that it gives us a specific domain, serverless-api-viewer.pages.dev. If I go here you can see that I get this unique URL that allows me to use this UI in production.

[0:54] This is important because we can use this as the origin in our CORS header to lock down what clients can use our serverless API. Then Access-Control-Allow-Origin and I'm going to replace that with my serverless API viewer.pages.dev.

[1:09] I'll save that and I'll deploy one more time to my serverless API function. If I come back here and I search something like food, you can see that things still look the same. Things are still working. I can still access the API.

[1:23] If I come back to the local host version of my React application, the one that's running at localhost:3000 and I search food, you can see that it still gives me a CORS error. The issue is different than it was in the past, before it was a Allow-Origin not found.

[1:40] In this case it's a Allow-Origin mismatch. The reason for that is that my API is now allowing requests from serverless API viewer.pages.dev, but it isn't allowing it from localhost:3000, or any other origin for that matter.

[1:56] The conclusion is that you can use this Access-Control-Allow-Origin to be more specific about what origins you want to allow to use your serverless API. Right now my backend serverless API is only allowing requests from serverless API viewer.pages.dev.

[2:12] That works in production but I may want to add a list of origins that is, another place like localhost:3000 that indicate, these are the list of places or origins that can make requests to my serverless API.

[2:26] To do this I'm going to set up a list of allowed origins. This is going to be an array with two values. The first localhost:3000 and the second which is my page's URL. This is just two origins. This is my development and this is my production that will be allowed to access this API.

[2:47] Now, I need to set up the correct CORS headers response here. Because of the way the CORS headers work, I can set Access-Control-Allow-Origin to multiple origins. There also is an access control, allow origins, like a plural header key, so I need to parse in one of this allowed headers based on where the request is coming from.

[3:09] To do that, I'm going to turn this into a function and I'm going to prse in origin here. Then, I'm going to parse in that origin as the Access-Control-Allow-Origin into this object. I'll also wrap this with parenthesis to say that this function returns a object.

[3:25] Any place that I use CORS headers, for instance right here, and also down here in my options check, I need to look at the origin that's coming in I need to say, "Is this a valid origin?" If not, then return unauthorized error and if so return those CORS headers as I've previously set them up.

[3:45] To do that, I'm going to make a new function called, check origin. This is just going to take in the request and it's going to return a function. Now, what I can do is look for the origin header coming from that request. I'll say, "Const origin = request.headers.get."

[4:00] That will get the origin header that's coming with this request. Now, I need to check inside of this allowed origins and see if this origin is also inside of this array. To do that, I'm going to say, return allowed origins.find, which is going to loop through everything inside of this array and call a function for everything in there.

[4:28] I'm just going to say, allowed origin. What I'm going to do is say, allowed origin includes origin. What this will do is say, loop through this list of allowed origins and see if the origin header that set here matches one of these two values.

[4:46] If it does, then it will return that value here as the response from this function. Otherwise, it will return, no. Down here in the option section, I'll say, const allowed origin = check origin request. That will return the allowed origin here and then I'll parse in allowed origin into this CORS headers function.

[5:12] In fact, I can also copy this up here as well into my get images function and also parse that into the CORS header here. I'll run wrangler publish one more time. Then, I'll be able to make sure that depending on where I'm making a request from, if it matches one of those two origins, I'll be able to return a valid response back. Otherwise, I'll get a CORS error.

[5:36] Coming back to localhost:3000, I can say, foo make a request. You can see it works. It gives me a response back. Importantly, if I come back here and look at this original request that I make, this is that POST request and I look at the response headers, I'm getting Access-Control-Allow-Origin localhost:3000.

[5:56] If I come back here and I make another request in my serverless API viewer.pages.dev, which is my production endpoint, I can do another request here. Maybe I'll say, CORGI and say, search. If come back up here and look at this response in production, the response header Access-Control-Allow-Origin is set to pages.dev.

[6:17] Because we can't set multiple origins in this header, we have to look at what the origin of the request is to our serverless API, and then we need to return a specific Access-Control-Allow-Origin if that is indeed one of the origins that we want to allow to access our API.

[6:33] To give you a final example, I'll set up my local create React app server using port 4000. Technically a different origin than what I had before where I had set it to localhost:3000.

[6:44] Here in my browser, you can see that the same app is running at localhost:4000, but if I search CORGI here, it's going to give me a CORS error, so preflight invalid allow origin value. This technically works and stops your browser from being able to make request.

[6:59] Instead of allowing this, we should set an error up that just says that this request is unauthorized. To fix this, I'm going to change this function slightly where I'm going to not allow it to return null here, which is what it would return from allowed origins.find.

[7:15] Instead, just provide back one of these two allowed origins to say, the origin you tried to request isn't valid but for instance serverless API viewer.pages.dev is. To do that, instead of returning this directly, I'm going to capture this as a variable found origin.

[7:34] Then, I'm going to return back by saying, if found origin. Return that, else I want to return back one of these known origins. To do that, I'm going to rearrange this slightly by putting this production at origin first, and then I'm going to return that as allowed origins with the index of zero.

[7:58] This will fix it from returning a null Access-Control-Allow-Origin header here and will instead return a proper value. I can shorten this by using a ternary statement which is a shorthand for this, if else, syntax.

[8:15] I'll say, return found origin with a question mark saying, if there is a found origin, return this value, the found origin, else return allowed origins with an index of zero. That ternary statement will allow me to shorten it to a single line here that says, if there is a found origin, return it, else return allowed origins zero.

[8:39] When I publish this one more time, I can come back in my API here and make another request, and you can see that the CORS error here has, a allow origin mismatch, which is closer to what we want versus a validation error where this access allow origin was just null.

[8:55] This is technically a more proper way of handling this header value by saying, there is a null response coming back here where we don't know what the right origin is.

[9:05] Instead, we're now saying, this is the correct origin, so something like localhost:3000 or serverless API viewer.pages.dev. This is a correct origin. You're just not requesting it from one of those two places.

brett
brett
~ a year ago

includes seems like the wrong option for checkOrigin. Wouldn't someone be able to create a url that's like https://serverless-api-viewer.pages.dev.malicious.website.com and have it match?