Enable CORS in Tomcat bundle

How to enable Cross-Origin Resource Sharing (CORS) in Tomcat, and check it.

If you try to call the REST API from a page hosted on another domain than the one of the tomcat bundle, you will face some issues due to the 'same-origin policy' enforced by web browsers. For instance you may see in your browser a message such as:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at XXX. This can be fixed by moving the resource to the same domain or enabling CORS.

By configuring the CORS filter on the tomcat bundle, you will be able to access the Bonita REST API from a page hosted on a different domain from the one of the tomcat bundle.

Tomcat configuration

Add CORS filter

Edit the web.xml of the bonita.war to add the CORS filter: *Important Note:* to use this configuration, you will need to replace the ALLOWED_ORIGIN_LIST by your own allowed origin list.

<filter>
  <filter-name>CorsFilter</filter-name>
  <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
  <init-param>
    <param-name>cors.allowed.origins</param-name>
    <param-value>ALLOWED_ORIGIN_LIST</param-value>
  </init-param>

  <init-param>
	<param-name>cors.support.credentials</param-name>
	<param-value>true</param-value>
  </init-param>

  <init-param>
    <param-name>cors.allowed.methods</param-name>
   <param-value>GET, HEAD, POST, PUT, DELETE, OPTIONS</param-value>
  </init-param>

  <!-- List of the response headers other than simple response headers that the browser should expose to
    the author of the cross-domain request through the XMLHttpRequest.getResponseHeader() method.
    The CORS filter supplies this information through the Access-Control-Expose-Headers header. -->
  <init-param>
      <param-name>cors.exposed.headers</param-name>
      <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials,X-Bonita-API-Token</param-value>
  </init-param>

  <!-- The names of the supported author request headers. These are advertised through the Access-Control-Allow-Headers header.
    The CORS Filter implements this by simply echoing the requested value back to the browser.
  -->
  <init-param>
      <param-name>cors.allowed.headers</param-name>
      <param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,X-Bonita-API-Token</param-value>
  </init-param>

</filter>
...
<filter-mapping>
  <filter-name>CorsFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

*Important Note 1:* The filter must be inserted in the file webapps/bonita/WEB-INF/web.xml (not in the Tomcat conf/web.xml)

*Important Note 2:* It must be the first filter, inserted right after the </error-page> tag

Choose cookies SameSite Policy

By default, cookies will be automatically passed with a same-site request, or a cross-site top-level navigation with a "safe" HTTP method.
This default configuration aims to ensure that we respect the new standard cookie policy rules.

To understand those changes, see https://blog.chromium.org/2020/02/samesite-cookie-changes-in-february.html
Also note that this new policy will be definitively applied in the months to come for Chrome, and later for the other browsers, as you can see here https://blog.chromium.org/2020/04/temporarily-rolling-back-samesite.html

If you want to perform "unsafe" CORS requests (which means performing a POST/PUT/DELETE request) you will need to modify the tomcat conf/context.xml file, to set sameSiteCookies to "none" instead of "lax".

    ...
    <!-- default samesite cookies configuration, for CORS set sameSiteCookies to "none" and configure bundle for HTTPS  -->
    <CookieProcessor sameSiteCookies="none" />
    ...

Using sameSiteCookies="none" will also force you to use secure HTTP (HTTPS) on your Bonita server.
To explain this constraint, see https://blog.chromium.org/2019/10/developers-get-ready-for-new.html

HTML Example test page

Here is an example of html page that:

  • logs in using the loginservice,

  • get the current session, via the REST api resource system/session

  • edit a user password using the REST api resource identity/user

This page can be hosted on a different domain, and thanks to the CORS filter, the requests will be successfully processed.

*Important Note 1:* this example works on a bundle where the CSRF security filter is activated. As the header "X-Bonita-API-Token" is set with the "session apiToken".

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>CORS Demo</title>

        <script type="text/javascript">
			var bonitaServerPath;

            function loginToBonita() {
                var myHeaders = new Headers();
                myHeaders.append("Content-Type", "application/x-www-form-urlencoded");

                var urlencoded = new URLSearchParams();
                urlencoded.append("username", document.getElementById("username").value);
                urlencoded.append("password", document.getElementById("current-password").value);
                urlencoded.append("redirect", "false");


                var requestOptions = {
                    method: 'POST',
                    headers: myHeaders,
                    body: urlencoded,
                    redirect: 'follow',
                    credentials: 'include'
                };

                return fetch(bonitaServerPath + "/loginservice", requestOptions)
                    .then(result => {
						if (!result.ok) {
							throw Error(result.status);
						}
						return getAuthToken();})
                    .catch(error => {document.getElementById("error").innerHTML += "<br/> &#x26a0; Login error. " + error;});
            };

            function getAuthToken() {
                var myHeaders = new Headers();
                var requestOptions = {
                    method: 'GET',
                    headers: myHeaders,
                    credentials: 'include'
                };

                return fetch(bonitaServerPath + "/API/system/session/unusedId", requestOptions)
                    .then(response => {
						if (!response.ok) {
							throw Error(response.status);
						}
						return response.headers.get("x-bonita-api-token");})
                    .catch(error => {document.getElementById("error").innerHTML += "<br/> &#x26a0; Unable to retrieve authentication token from session. " + error;});
            };

            function getUserId() {
                var myHeaders = new Headers();
                var requestOptions = {
                    method: 'GET',
                    headers: myHeaders,
                    credentials: 'include'
                };

                return fetch(bonitaServerPath + "/API/system/session/unusedId", requestOptions)
                    .then(response => {
						if (!response.ok) {
							throw Error(response.status);
						}
						return response.json();})
                    .then(body => body.user_id)
                    .catch(error =>  {document.getElementById("error").innerHTML += "<br/> &#x26a0; Unable to retrieve UserId from session. " + error;});
            };

            function updatePassword(authToken) {
                var formData = {"password": document.getElementById("new-password").value}
                var myHeaders = new Headers();
                myHeaders.append("X-Bonita-API-Token", authToken);
                myHeaders.append("Content-Type", 'application/json');

                var requestOptions = {
                    method: 'PUT',
                    headers: myHeaders,
                    credentials: 'include',
                    body: JSON.stringify(formData)
                };


                return getUserId().then(userId =>
                    fetch(bonitaServerPath + "/API/identity/user/" + userId, requestOptions)
                        .then(response => {
							if (!response.ok) {
								throw Error(response.status);
							}
							return response.text();})
                        .then(result =>  {document.getElementById("success").innerHTML = "&#10003; Password updated!"})
                        .catch(error =>  {document.getElementById("error").innerHTML += "<br/> &#x26a0; Unable to update the password. " + error;}));

            };

            function submit() {
				document.getElementById("success").innerHTML = "";
				document.getElementById("error").innerHTML = "";
				bonitaServerPath = document.getElementById("bonita-server-path").value;
				loginToBonita().then(authToken => updatePassword(authToken));
            };
        </script>

    </head>
    <body>
		<div style="display: flex; flex-direction: column;  align-items: center;">
				<h1>CORS Demo, edit user password:</h1>
				<div>
					<label style="width: 150px; display:inline-block;  padding: 5px 0;" for="bonita-server-path">Path to bonita server</label></span>
					<input type="text" placeholder="Enter bonita server path" id="bonita-server-path" required/>
				</div>
				<div>
					<label style="width: 150px; display:inline-block;  padding: 5px 0;" for="username">Username</label></span>
					<input type="text" placeholder="Enter username" id="username" required/>
				</div>
				<div>
					<label style="width: 150px; display:inline-block; padding: 5px 0;" for="username">Current password</label>
					<input type="password" placeholder="Enter current password" id="current-password" required/>
				</div>
				<div>
					<label style="width: 150px; display:inline-block; padding: 5px 0;" for="username">New password</label>
					<input type="password" placeholder="Enter new password" id="new-password" required/>
				</div>
				<button  style="margin: 5px 0;" onclick="submit()">Update password</button>
				<div style="width: 320px;">
					<p style="color:green; padding: 5px 0;" id="success"></p>
					<p style="color:red; padding: 5px 0;" id="error"></p>
				</div>
		</div>
    </body>
</html>