Configure CORS in API Management

azure api management service wallpaper

In this post, I will show you how to configure CORS in the API Management Service in Azure. If you read my previous post, I shown how to create an Azure API Management Service. So, I faced an issue with CORS that I want to share because I think could be useful for someone else.

The problem

Happily, I have just configured my API Management Service and connect the APIs. Now, I want to use them from my applications. From C# is working fine. If I try a GET request in the browser, it is working. If I try to call the APIs from a JavaScript code I receive this error:

XMLHttpRequest cannot load https://www.example.com/ No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘https://localhost:4300’ is therefore not allowed access.

or another one is

Access to fetch at ‘https://test.test.test.com/subs/v12/2Subs’ from origin ‘null’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

Developer tools with CORS the errors - Configure CORS in API Management
Developer tools with CORS the errors

The JavaScript code is use it is pretty simple

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Test Api</title>
    </head>
    <body>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script>
        var url = 'https://api.mydomain.com/api/';
        var headerKey = 'Apim-Subscription-Key';
        var headerValue = 'your-subscription-key';
        
        // XMLHttpRequest preflight request
        var xhr = new XMLHttpRequest();
        xhr.open("GET", url, true);
        xhr.setRequestHeader(headerKey, headerValue);
        xhr.onload = function () {
            console.log(xhr.responseText);
            alert("Complete1");
        };
        xhr.send();
        
        // Fetch preflight request
        var myHeaders = new Headers();
        myHeaders.append(headerKey, headerValue);
        fetch(url, {
            headers: myHeaders
        }).then(function (response) {
            return response.json();
        }).then(function (json) {
            console.log(json);
            alert("Complete2");
        });
        </script>
    </body>
</html>

About the Same Origin Policy

This is the Same Origin Policy. It is a security feature implemented by browsers.

The standard scenario that demonstrates the need for the SOP can be demonstrated with three characters:

  • Alice is a person with a web browser
  • Bob runs a website (https://www.[website].com/)
  • Mallory runs a website (https://localhost:4300)

Alice is logged into Bob’s site and has some confidential data there. Perhaps it is a company intranet (accessible only to browsers on the LAN). Or her online banking (accessible only with a cookie you get after entering a username and password).

Alice visits Mallory’s website which has some JavaScript. That causes Alice’s browser to make an HTTP request to Bob’s website (from her IP address with her cookies, etc). This could be as simple as using XMLHttpRequest and reading the responseText.

The browser’s Same Origin Policy prevents that JavaScript from reading the data returned by Bob’s website (which Bob and Alice don’t want Mallory to access). (Note that you can, for example, display an image using an <img> element across origins because the content of the image is not exposed to JavaScript (or Mallory) … unless you throw canvas into the mix in which case you will generate a same-origin violation error).

Why the Same Origin Policy applies when you don’t think it should

For any given URL it is possible that the SOP is not needed. A couple of common scenarios where this is the case are:

  • Alice, Bob and Mallory are the same person.
  • Bob is providing entirely public information

… but the browser has no way of knowing if either of the above are true. So, trust is not automatic and the SOP is applied. Permission has to be granted explicitly before the browser will give the data it was given to a different website.

Why the Same Origin Policy only applies to JavaScript in a web page

Browser extensions, the Network tab in browser developer tools and applications like Postman are installed software. They aren’t passing data from one website to the JavaScript belonging to a different website just because you visited that different website. Installing software usually takes a more conscious choice. There isn’t a third party (Mallory) who is considered a risk.

Browser extensions do need to be written carefully to avoid cross-origin issues. See the Chrome documentation for example.

Why you can display data in the page without reading it with JS

There are a number of circumstances where Mallory’s site can cause a browser to fetch data from a third party and display it (e.g. by adding an <img> element to display an image). It isn’t possible for Mallory’s JavaScript to read the data in that resource though. Only Alice’s browser and Bob’s server can do that, so it is still secure.

CORS

The Access-Control-Allow-Origin HTTP response header referred to in the error message is part of the CORS standard which allows Bob to explicitly grant permission to Mallory’s site to access the data via Alice’s browser.

A basic implementation would just include:

Access-Control-Allow-Origin: *

and in the response headers to permit any website to read the data.

Access-Control-Allow-Origin: https://example.com/

would allow only a specific site to access it, and Bob can dynamically generate that based on the Origin request header to permit multiple, but not all, sites to access it.

The specifics of how Bob sets that response header depend on Bob’s HTTP server and/or server-side programming language. There is a collection of guides for various common configurations that might help.

Model of where CORS rules are applied

Some requests are complex and send a preflight OPTIONS request. The server will have to respond to before the browser will send the GET/POST/PUT/Whatever request that the JS wants to make. Implementations of CORS that only add Access-Control-Allow-Origin to specific URLs often get tripped up by this.

Obviously granting permission via CORS is something Bob would only do only if either:

  • The data was not private or
  • Mallory was trusted

But I’m not Bob!

There is no standard mechanism for Mallory to add this header because it has to come from Bob’s website, which she does not control.

If Bob is running a public API then there might be a mechanism to turn on CORS. This will have to be a mechanism implemented by Bob though. Mallory could read the documentation on Bob’s site to see if something is available, or she could talk to Bob and ask him to implement CORS.

Error messages which mention “Response for preflight”

Some cross origin requests are preflighted.

This happens when (roughly speaking) you try to make a cross-origin request that:

  • Includes credentials like cookies
  • Couldn’t be generated with a regular HTML form (e.g. has custom headers or a Content-Type that you couldn’t use in a form’s enctype).

If you are correctly doing something that needs a preflight

In these cases then the rest of this answer still applies but you also need to make sure that the server can listen for the preflight request (which will be OPTIONS (and not GETPOST or whatever you were trying to send) and respond to it with the right Access-Control-Allow-Origin header but also Access-Control-Allow-Methods and Access-Control-Allow-Headers to allow your specific HTTP methods or headers.

If you are triggering a preflight by mistake

Sometimes people make mistakes when trying to construct Ajax requests, and sometimes these trigger the need for a preflight. If the API is designed to allow cross-origin requests, but doesn’t require anything that would need a preflight, then this can break access.

Common mistakes that trigger this include:

  • trying to put Access-Control-Allow-Origin and other CORS response headers on the request. These don’t belong on the request, don’t do anything helpful (what would be the point of a permissions system where you could grant yourself permission?), and must appear only on the response.
  • trying to put a Content-Type: application/json header on a GET request that has no request body to describe the content of (typically when the author confuses Content-Type and Accept).

In either of these cases, removing the extra request header will often be enough to avoid the need for a preflight (which will solve the problem when communicating with APIs that support simple requests but not preflighted requests).

Opaque responses

Sometimes you need to make an HTTP request, but you don’t need to read the response. e.g. if you are posting a log message to the server for recording.

If you are using the fetch API (rather than XMLHttpRequest), then you can configure it to not try to use CORS.

Note that this won’t let you do anything that you require CORS to do. You will not be able to read the response. You will not be able to make a request that requires a preflight.

It will let you make a simple request, not see the response, and not fill the Developer Console with error messages.

How to do it is explained by the Chrome error message given when you make a request using fetch and don’t get permission to view the response with CORS:

Access to fetch at ‘https://example.com/‘ from origin ‘https://example.net‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin‘ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

Thus:

fetch("https://example.com", { mode: "no-cors" });

Alternatives to CORS

JSONP

Bob could also provide the data using a hack like JSONP which is how people did cross-origin Ajax before CORS came along.

It works by presenting the data in the form of a JavaScript program which injects the data into Mallory’s page. It requires that Mallory trust Bob not to provide malicious code.

Note the common theme: The site providing the data has to tell the browser that it is OK for a third party site to access the data it is sending to the browser.

Since JSONP works by appending a <script> element to load the data in the form of a JavaScript program which calls a function already in the page, attempting to use the JSONP technique on a URL which returns JSON will fail — typically with a CORB error — because JSON is not JavaScript.

Move the two resources to a single Origin

If the HTML document the JS runs in and the URL being requested are on the same origin (sharing the same scheme, hostname, and port) then they Same Origin Policy grants permission by default. CORS is not needed.

A Proxy

Mallory could use server-side code to fetch the data (which she could then pass from her server to Alice’s browser through HTTP as usual).

It will either:

  • add CORS headers
  • convert the response to JSONP
  • exist on the same origin as the HTML document

That server-side code could be written & hosted by a third party (such as CORS Anywhere). Note the privacy implications of this: The third party can monitor who proxies what across their servers.

Bob wouldn’t need to grant any permissions for that to happen.

There are no security implications here since that is just between Mallory and Bob. There is no way for Bob to think that Mallory is Alice and to provide Mallory with data that should be kept confidential between Alice and Bob.

Consequently, Mallory can only use this technique to read public data.

Do note, however, that taking content from someone else’s website and displaying it on your own might be a violation of copyright and open you up to legal action.

Writing something other than a web app

As noted in the section “Why the Same Origin Policy only applies to JavaScript in a web page”, you can avoid the SOP by not writing JavaScript in a webpage.

That doesn’t mean you can’t continue to use JavaScript and HTML, but you could distribute it using some other mechanism, such as Node-WebKit or PhoneGap.

Browser extensions

It is possible for a browser extension to inject the CORS headers in the response before the Same Origin Policy is applied.

These can be useful for development, but are not practical for a production site (asking every user of your site to install a browser extension that disables a security feature of their browser is unreasonable).

They also tend to work only with simple requests (failing when handling preflight OPTIONS requests).

Having a proper development environment with a local development server is usually a better approach.

Other security risks

Note that SOP / CORS do not mitigate XSSCSRF, or SQL Injection attacks which need to be handled independently.

When the problem starts

So, this issue happens when you enable the Developer portal. Then, in the configuration for the portal, you enable CORS. At this point, Azure automatically adds a global policy to the API Management Service but you don’t know that.

<policies>
    <inbound>
        <!-- base: Begin Global scope -->
        <cors allow-credentials="true">
            <allowed-origins>
                <origin>https://developer.mydomain.com</origin>
            </allowed-origins>
            <allowed-methods preflight-result-max-age="300">
                <method>*</method>
            </allowed-methods>
            <allowed-headers>
                <header>*</header>
            </allowed-headers>
            <expose-headers>
                <header>*</header>
            </expose-headers>
        </cors>
        <!-- base: End Global scope -->
    </inbound>
</policies>

Happily, you configure CORS in your API and when you try to call them from JavaScript, you are facing the CORS error.

If you open the CORS configuration in the Azure portal you can see something like that:

<policies>
    <inbound>
        <base />
        <cors>
            <allowed-origins>
                <origin>*</origin>
            </allowed-origins>
            <allowed-methods preflight-result-max-age="300">
                <method>GET</method>
                <method>POST</method>
            </allowed-methods>
            <allowed-headers>
                <header>*</header>
                <header />
            </allowed-headers>
            <expose-headers>
                <header>*</header>
            </expose-headers>
        </cors>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

My understanding is the portal combine the base configuration with yours. So, like in Active Directory the more restricted policy is applying. That means in my example, only the Developer Portal can call the APIs without CORS issue.

Solution

Delete <base /> under <inbound> and save.

One thought on “Configure CORS in API Management

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.