Ghost, Digital Ocean, Cloudflare, 403 on ghost/api/admin/users/me/?include=roles

Hi,

Context
We are using the Digitalocean Ghost image.

We are using Cloudflare’s DNS blog. type A record with Proxy Status = DNS only.

Now everything’s working great (the app is loading), but when we access domain.com/blog/ghost or blog.domain.com/blog/ghost, we see the following error below.

{
    "errors": [
        {
            "message": "Authorization failed",
            "context": "Unable to determine the authenticated user or integration. Check that cookies are being passed through if using session authentication.",
            "type": "NoPermissionError",
            "details": null,
            "property": null,
            "help": null,
            "code": null,
            "id": "e6518360-4a0f-11ed-b437-dba7f71d3857",
            "ghostErrorCode": null
        }
    ]
}

I have checked the nginx.conf, and it has the default configuration from these templates Ghost-CLI/ssl-params.conf at main · TryGhost/Ghost-CLI · GitHub

Ghost config

ghost-mgr@ghostonubuntu2204-s-1vcpu-1gb-fra1-01:/var/www/ghost$ cat config.production.json 
{
  "url": "https://blog.domain.com/blog",
  "server": {
    "port": 2368,
    "host": "127.0.0.1"
  },
  "database": {
    "client": "mysql",
    "connection": {
      "host": "localhost",
      "user": "ghost-651",
      "password": "###",
      "port": 3306,
      "database": "ghost_production"
    }
  },
  "mail": {
    "transport": "Direct"
  },
  "logging": {
    "transports": [
      "file",
      "stdout"
    ]
  },
  "process": "systemd",
  "paths": {
    "contentPath": "/var/www/ghost/content"
  }
}

Any suggestions?

Warm regards :3

The 403 is expected, nothing to worry about. When you access the admin app it makes a request to the API to get the logged-in staff user details, if you’re not logged in the API responds with a 403 and the admin app knows to show the login screen.

It works on this domain: blog.domain.com/blog/ghost

But now we have encountered another issue.

We use Cloudflare’s worker for every access of the domain.com/blog* route.

We fetch the subdomain blog.domain.com/blog page and show it on the root domain domain.com/blog

The code of the worker.

const GHOST_CONFIG = {
  SUBDOMAIN: "blog.domain.com",
  ROOT: "domain.com",
  BLOG_PATH: "blog"
}

// Function that processes requests to the URL the worker is at
async function handleRequest(request) {
  // Grab the request URL's pathname, we'll use it later
  const url = new URL(request.url);
  const targetPath = url.pathname;

  let response = await fetch(`https://${GHOST_CONFIG.SUBDOMAIN}${targetPath}`);
  console.log(response);

  // Ghost loads assets like JS and CSS from 3 subdirectories
  // We don't need to change these requests at all
  // So if we're getting stuff from those subdirectories,
  // we return the response of the fetch request from above
  // immediately.
  if (
      targetPath.includes(`/${GHOST_CONFIG.BLOG_PATH}/favicon.png`) ||
      targetPath.includes(`/${GHOST_CONFIG.BLOG_PATH}/sitemap.xsl`) ||
      targetPath.includes(`/${GHOST_CONFIG.BLOG_PATH}/assets/`) ||
      targetPath.includes(`/${GHOST_CONFIG.BLOG_PATH}/public/`) ||
      targetPath.includes(`/${GHOST_CONFIG.BLOG_PATH}/content/`)
  ) { 
      return response
  }

  // In other cases - which will usually be pages of the
  // Ghost blog - we want to find any reference to our subdomain
  // and replace it with our root domain.
  // This is so that things like our canonical URLs and links are
  // set up correctly, so we NEVER see our subdirectory in the code.

  // First we get the body of the response from above
  let body = await response.text()

  // Then we search in the body to replace the subdomain everywhere
  // with the root domain.
  body = body.split(GHOST_CONFIG.SUBDOMAIN).join(GHOST_CONFIG.ROOT)

  response = new Response(body, response)
  return response
}

This is the error I get on the attempt to log in via this endpoint https://domain.com/blog/ghost/api/admin/session

{
    "errors": [
        {
            "message": "Authorization failed",
            "context": "Unable to determine the authenticated user or integration. Check that cookies are being passed through if using session authentication.",
            "type": "NoPermissionError",
            "details": null,
            "property": null,
            "help": null,
            "code": null,
            "id": "63754bb0-4a1a-11ed-b437-dba7f71d3857",
            "ghostErrorCode": null
        }
    ]
}

So the question is — is the problem because this setup forges the request and cookies are not passed properly, resulting in auth error?

Do you have the url config in Ghost set to domain.com/blog? If the url doesn’t match where you’re actually serving the site you’ll run into problems due to domain mismatches. Configuring the site properly like that will also mean you don’t need to do any rewrites in your workers and you’re closer to the supported way of proxying a subdirectory site.

The url of the Ghost is set to https://blog.domain.com/blog.

Correct, the URL does not match because we intend users to open domain.com/blog. We want it to be specifically on the root domain for SEO purposes.

Configuring the site properly like that will also mean you don’t need to do any rewrites in your workers, and you’re closer to the supported way of proxying a subdirectory site.

I think I understand your point. We have such a setup because we are hosted on notion pages on our root domain, domain.com, so we use 2 Cloudflare workers for domain.com/* (notion pages) and domain.com/blog* (Ghost).

I’ll try to do it under one worker with the root domain.

I see a couple of things you could do :

  1. In your config, set url to https://domain.com/blog
    • Since you’re planning on sharing https://domain.com/blog with everyone, Ghost has to be configured like this. Otherwise, some of the links that show up on the page will also point to the wrong place
  2. [optional] update admin.url to https://blog.domain.com/blog. Then, have your staff access the admin via https://blog.domain.com/blog/ghost/. More information on the admin URL: Configuration - Adapt your publication to suit your needs

Is there a way to update Ghost CORS policy on response with the list of allowed domains?

  • Access-Control-Allow-Origin indicates what origin can fetch resources. Use one or more origins, e.g.: https://foo.io,http://bar.io.

The CORS policies have changed quite a bit since I last looked into them, so I’m not sure. I don’t think you can change the CORS policies, but I do think they’re designed to allow proper communication if you have between a separate frontend and backend, if you’ve properly configured them