Monday, December 14, 2020

Zato Blog: Configuring REST channels for CORS

With the rise of Single-Page Applications (SPA) in web frontends, it is often the case that backend REST APIs based on Zato need to be configured for CORS. This article will explore what CORS is and how to make Zato participate in scenarios using it.

REST channel web-admin menu

Terminology

CORS, as an acronym, has several parts:

  • Cross-Origin
  • Resource
  • Sharing

In the context of REST APIs, which predominantly means JSON or XML served usually with POST or other calls, this can be simplified to the below.

Origin means a combination of a URL scheme, hostname and port number. For instance, https://example.com is one origin whereas https://example.com:8443 is a different one (ports differ). Similarly, https://www.example.net is different than https://api.example.net (subdomains differ). Also, http:// vs. https:// is a different URL scheme and that means a different origin too even if domains and ports were the same.

Resource, here, is a REST API endpoint that is accessible via HTTP to JavaScript calls such as XMLHttpRequest (XHR). Note that XHR is not the only mechanism used but, because it is the most popular one in practice today, this article will concern with XHR only.

Sharing is the act of making resources available to XHR calls.

Finally, we deal with cross-origin sharing which means that JavaScript code and the REST endpoint will be served from different origins. E.g. https://www.example.com for the frontend where XHR calls come from whereas https://api.example.com is used for the REST endpoints.

Putting it together, in the most commonly found case, we have a frontend web page served from one domain and a REST API served from another domain and we want for the frontend to make XHR calls to the REST endpoints. CORS is the technique employed to make it happen. Without CORS, we would encounter a restriction called Same-Origin Policy (SOP) which would prevent the calls from succeeding.

However, note that the restriction does not apply to, for instance, resources loaded via <script src="..."> elements. This is why libraries like JQuery can be hot-linked to using CDNs or other locations but regular calls to one's REST APIs may require CORS configuration. Likewise, browser extensions may be exempt from SOP.

In short, in CORS, we deal with XHR and similar mechanisms specifically and other features that browsers have to offer may require it or not - do not be surprised if SOP and CORS are used in one place but not in another, outside of the realm of REST APIs invoked from pages served by web servers which is what this article covers.

Also, note that it is the web browser that CORS and SOP are enforced by - it means that if an endpoint is accessed via another HTTP client, such as curl, the restrictions will not apply.

Two types of CORS calls in REST APIs

A browser will issue two types of XHR calls:

  • Simple ones, ususually not used in REST APIs built around JSON or XML
  • Complex ones, called pre-flighted ones, most often used with JSON or XML

With simple calls, an XHR call is made and certain specific headers will need to exist for the XHR caller to be able to read the response from the API server.

With complex ones, the browser first sends a short OPTIONS request (pre-flight), asking the server if the actual request can be sent. If the response is satisfactory, the browser will send a second request, this time with the real data that the XHR user meant to send.

Note that CORS is handled by browsers automatically. For instance, when an XHR POST call is issued, it is the browser that sends OPTIONS under the hood and the frontend developer does not control whether it happens or not.

Thus, the server's job is to return headers expected in a given type of a CORS-using invocation. The headers tell the browser whether it can proceed and if so, the browser continues in the process, ultimately returning the response from an API call to JavaScript code.

Keep in mind that, from the browser's perspective, CORS is not about preventing an XHR caller from invoking specific APIs. Rather, it is about preventing responses to such calls from reaching to the XHR caller. This means that in server logs you may notice GET, OPTIONS, POST or other invocations even if the browser does not deliver them to JavaScript.

At this point, it is best to read the Mozilla article about CORS. It goes into all the details and, in fact, given that it is concerned with the web as a whole, some parts of it do not translate directly into REST APIs and Zato but the knowledge is required nevertheless to make sure that the configuration is correct.

The rest of this blog post will assume that you now know what to configure and only a way to do it with Zato needs to be shown.

Configuring Zato for CORS

Configuration of Zato services can be condensed to two main choices:

  • We can use the after_handle response hook
  • We can add handle_OPTIONS

Consider the code below. It uses after_handle - this is a method that is invoked each time the handle menthod has just finished. We can take advantage of it to inject response HTTP headers.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService(Service):

    def handle(self):
        # Business implementation goes here
        pass

    def after_handle(self):

        # We only allow requests from this particular origin
        allow_from_name = 'Access-Control-Allow-Origin'
        allow_from_value = 'https://www.example.com'

        self.response.headers[allow_from_name] = allow_from

Now, the same but using handle_OPTIONS.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService2(Service):

    def handle(self):
        # Business implementation goes here
        pass

    def handle_OPTIONS(self):

        # We only allow requests from this particular origin
        allow_from_name = 'Access-Control-Allow-Origin'
        allow_from_value = 'https://www.example.com'

        self.response.headers[allow_from_name] = allow_from

The difference between the two is not large. Mainly, after_handle runs regardless whether the channel is invoked using REST, SOAP, IBM MQ, AMQP, WebSockets or any other possible which means that in some cases it may be superfluous.

On the other hand, with REST channels, after_handle runs no matter what input HTTP verb the request is using while handle_OPTIONS reacts to OPTIONS only so after_handle may be used for both simple and pre-flight CORS configuration.

All things considered, which one to use will be a choice to make based on a given integration scenario - both are applicable, depending on specific requirements.

Note also that in the code above each service has its own CORS configuration. Yet, it is likely that the same configuration should be shared by multiple services. Simple Python inheritance will work here nicely:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class BaseService(Service):

    def after_handle(self):

        # We only allow requests from this particular origin
        allow_from_name = 'Access-Control-Allow-Origin'
        allow_from_value = 'https://www.example.com'

        self.response.headers[allow_from_name] = allow_from

class MyChildService1(BaseService):

    def handle(self):
        # Business implementation goes here
        pass

class MyChildService2(BaseService):

    def handle(self):
        # Business implementation goes here
        pass

Or, with handle_OPTIONS:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class BaseService(Service):

    def handle_OPTIONS(self):

        # We only allow requests from this particular origin
        allow_from_name = 'Access-Control-Allow-Origin'
        allow_from_value = 'https://www.example.com'

        self.response.headers[allow_from_name] = allow_from

class MyChildService1(BaseService):

    def handle(self):
        # Business implementation goes here
        pass

class MyChildService2(BaseService):

    def handle(self):
        # Business implementation goes here
        pass

Now, the same configuration is accessible to more than one service and if anything needs to be changed, it is done in the base service.

Observe that the code above uses 'Access-Control-Allow-Origin' only but CORS employs other headers as well - you just put them all in self.response.headers, it is a regular Python dictionary with keys mapping to HTTP header names and values to header values.

This is everything that is needed to configure Zato for CORS-using REST API calls - choose the method most suitable (after_handle or handle_OPTIONS), assign headers to self.response.headers, hot-deploy your service and that is it!



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...