Add CORS header for WP Rest API in nginx-includes with a group_vars variable

Hi there,

On a running project we’re using Wordpress as a headless CMS using only the WP Rest API to get data in a React frontend using Next.js. For the frontend we’re using Docker Compose with Gitlab CI for deployment and for the backend Trellis.

The frontend & backend are available on 3 environments, on (as example) these domains:

http://mydomain.test         => http://admin.mydomain.test 
https://staging.mydomain.io  => http://admin.staging.mydomain.io
https://mydomain.io          => http://admin.mydomain.io

Each Trellis server should only be allowed to return the WP Rest API get requests from the matching environment, so I added a variable name in every group_vars environment called front_url:

wordpress_sites:
  admin.mydomain.io:
    site_hosts:
      - canonical: admin.mydomain.test
        redirects:
          - www.admin.mydomain.test
    local_path: ../site
    multisite:
      enabled: false
    ssl:
      enabled: false
      provider: self-signed
    cache:
      enabled: false
    env:
      front_url: http://mydomain.test
      front_domain: mydomain.test

In my headless WP theme, I set the CORS header based upon this env var:

function get_frontend_origin() {
	return FRONT_URL;
}

add_action( 'rest_api_init', function() {

	remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');

	add_filter('rest_pre_serve_request', function($value) {
		header('Access-Control-Allow-Origin: ' . get_frontend_origin());
		header('Access-Control-Allow-Methods: GET');
		header('Access-Control-Allow-Credentials: true');

		return $value;
	});
}, 15);

Which works great!

Now my problem started when I needed an extra subdomain on my frontend that should also be able to use my WP Rest API:

http://mydomain.test,
http://sub.mydomain.test         => http://admin.mydomain.test
https://sub.staging.mydomain.io,
https://staging.mydomain.io      => http://admin.staging.mydomain.io
https://sub.mydomain.io,
https://mydomain.io              => http://admin.mydomain.io

Since it’s not possible to use wildcards in the Access-Control-Allow-Origin header, I tried to use the
$_SERVER['HTTP_ORIGIN'] value and check that against an array of allowed domains, but for some reason the $_SERVER['HTTP_ORIGIN'] value is always empty?

So then I thought, I will remove these CORS settings in PHP and will try to add it in a nginx-includes template in Trellis and bumped into this example to set the headers based upon the $http_origin.

So I added this structure in the root of my trellis folder:

trellis
└──nginx-includes/
     └── admin.mydomain.io/
          └── cors.conf.j2

With the following rules in cors.conf.j2:

location ~ ^/wp-json/ {
	set $cors "";

	if ($http_origin ~* (.*\.{{ site.env.front_domain }})) {
		set $cors "true";
	}

	if ($cors = "true") {
		add_header 'Access-Control-Allow-Origin' "$http_origin";
		add_header 'Access-Control-Allow-Credentials' 'true';
		add_header 'Access-Control-Allow-Methods' 'GET';
		add_header 'Access-Control-Allow-Headers' 'User-Agent,Keep-Alive,Content-Type';
	}
}

But a re-provision results in an error:

AnsibleUndefinedVariable: 'site' is undefined

So my main question is, how can I use my front_domain variable from group_vars in my nginx-includes Jinja2 template like this?

Since the template is already in it’s separate site folder, how can I access it’s group_vars?
Any help is welcome, thanks!

Ok by writing my post I realized what I did wrong here, in order to get the site specific group_vars I need to use a child template, so I added the following file:

trellis
└──nginx-includes/
     └── admin.mydomain.io.conf.child

and added the file in my group_vars:

wordpress_sites:
  admin.mydomain.io:
    nginx_wordpress_site_conf: nginx-includes/admin.mydomain.io.conf.child
    site_hosts:
      - canonical: admin.mydomain.test
        redirects:
          - www.admin.mydomain.test
    local_path: ../site
    multisite:
      enabled: false
    ssl:
      enabled: false
      provider: self-signed
    cache:
      enabled: false
    env:
      front_url: http://mydomain.test
      front_domain: mydomain.test

My child template file looks like this now:

{% extends 'roles/wordpress-setup/templates/wordpress-site.conf.j2' %}

{% block h5bp -%}
  {{ super() }}

  location ~ ^/wp-json/ {
    set $cors "";

    if ($http_origin ~* (.*\.{{ item.value.env.front_domain }})) {
      set $cors "true";
    }

    if ($cors = "true") {
      add_header 'Access-Control-Allow-Origin' "$http_origin";
      add_header 'Access-Control-Allow-Credentials' 'true';
      add_header 'Access-Control-Allow-Methods' 'GET';
      add_header 'Access-Control-Allow-Headers' 'User-Agent,Keep-Alive,Content-Type';
    }
  }
{% endblock %}

When I re-provision and check my /etc/nginx/sites-available/admin.mydomain.io.conf file, it prints the item.value.env.front_domain value correctly:

location ~ ^/wp-json/ {
    set $cors "";

    if ($http_origin ~* (.*\.mydomain.test)) {
      set $cors "true";
    }

    if ($cors = "true") {
      add_header 'Access-Control-Allow-Origin' "$http_origin";
      add_header 'Access-Control-Allow-Credentials' 'true';
      add_header 'Access-Control-Allow-Methods' 'GET';
      add_header 'Access-Control-Allow-Headers' 'User-Agent,Keep-Alive,Content-Type';
    }
  }

But now, all my WP Rest API requests result in an 404 Not Found page…
Any idea what I’m doing wrong here?

Thanks!

Ok, in case anyone else bumps into this;

The reason why $_SERVER['HTTP_ORIGIN'] was empty, was because I was making the WP Rest API request on the server side in next.js, so the origin was empty.

If I pass the origin in the headers object:

import fetch from 'isomorphic-unfetch';

// passed from express
const origin = req.protocol + '://' + req.get('host');
let page;

await fetch(`https://admin.mydomain.io/wp-json/my-app/my-endpoint`, {
	headers: { origin }
})
.then(result => result.json())
.then(result => {
	page = result;
}).catch(error => {
	console.log(error)
});

I can make the check in Wordpress:

add_action('rest_api_init', function() {

	remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
	add_filter('rest_pre_serve_request', function($value) {

		// Get http origin
		$origin  = get_http_origin();
		$domains = [
			$_SERVER['FRONT_URL'], 	  // Set in .env file
			$_SERVER['SUBDOMAIN_URL'] // Set in .env file
		];

		if ($origin && in_array($origin, $domains)) {
			header( 'Access-Control-Allow-Origin: ' . esc_url_raw($origin));
			header( 'Access-Control-Allow-Methods: GET, OPTIONS');
			header( 'Access-Control-Allow-Credentials: true');
		}

		return $value;
	});
}, 15);

Now my WP Rest API only accepts GET requests from my pre-defined (sub)domains!

2 Likes

Thanks a lot for sharing this, I’m about to hit the same issue on a multisite setup so this gives me a great head start!

1 Like

This topic was automatically closed after 42 days. New replies are no longer allowed.