API V2 - CORS/Trusted Client Domains - Solved

  • Version 2.16.4

I’m trying to access Version 2 Admin API, when I do a curl request the data returns correctly however if do a http request using javascript I get connection refused. I suspect this is a Cors Header issue as I’m making the request from a different domain the blog is running on. Nevertheless I’m not sure if Trusted Client Domains is still in use in Version 2.


curl -H "Authorization: 'Ghost $token'" https://{admin_domain}/ghost/api/{version}/admin/{resource}/

Doesn’t Work -

  <script type="text/javascript">
    var data = null;

    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;

    xhr.addEventListener("readystatechange", function () {
      if (this.readyState === 4) {

    xhr.open("GET", "http://localhost/ghost/api/v2/admin/posts/?limit=1");
    xhr.setRequestHeader("Authorization", "Ghost yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVjODU1ZTAwYmM1MzNlMDAwMWJmZjkwNyJ9.eyJpYXQiOjE1NTIyNzQ0NTMsImV4cCI6MTU1MjI3NDc1MywiYXVkIjoiL3YyL2FkbWluLyJ9.tOk3sxjWjfxm5PEmevyj_zYULCFXoYFyLeBRFyJ7tq0");
    xhr.setRequestHeader("cache-control", "no-cache");


Error I get is

Access to XMLHttpRequest at 'http://localhost/ghost/api/v2/admin/posts/?limit=1' from origin 'http://localhost:8888' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:8888, *', but only one is allowed.

Or when I use Nodejs example https://docs.ghost.org/api/admin/ the error I get is

Error: connect ECONNREFUSED

@Extracreative the v2 Admin API with token authentication should never be used from client-side code as that would be insecure because the private api key becomes public. Perhaps you’re looking for the v2 Content API instead? If not maybe you could explain your use case?


@Kevin Thank you for responding. Where would I run the below as I’m currently trying to run it from an nodejs docker container (server-side)? Is the trusted client till in use? Does this have to be run on the same domain as my ghost blog domain?

I want to post data to ghost blog and create pages and post from an external source

const jwt = require('jsonwebtoken');
const axios = require('axios');

// Admin API key goes here
const key = 'YOUR_ADMIN_API_KEY';

// Split the key into ID and SECRET
const [id, secret] = key.split(':');

// Create the token (including decoding secret)
const token = jwt.sign({}, Buffer.from(secret, 'hex'), {
    keyid: id,
    algorithm: 'HS256',
    expiresIn: '5m',
    audience: `/v2/admin/`

// Make an authenticated request
const url = 'http://localhost:2368/ghost/api/v2/admin/posts/?limit=1';
const headers = { Authorization: `Ghost ${token}` };
axios.get(url, { headers })
    .then(response => console.log(response))
    .catch(error => console.error(error));

Is the trusted client till in use?

No, the v2 APIs do not use trusted clients at all.

Where would I run the below as I’m currently trying to run it from an nodejs docker container (server-side)? Does this have to be run on the same domain as my ghost blog domain?

If you’re running it server-side in nodejs then there’s no CORS to worry about (that is a browser technology) and you can run it from anywhere as domains aren’t involved.

Is there a reason you’re not using the Node.js client library for the admin API? You haven’t explained what error messages you’re getting when you try and your node.js code vs the earlier browser example.


@Kevin Thanks again.

I’m running the Token Generation Javascript Example (Found on Ghost Admin API Doc) in an express js docker container accessible on http://localhost:3000.

My Ghost Blog is set up and running on http://localhost:2368.

When I hit the express service at http://localhost:3000/ghost-post it runs this Token Generation Javascript Example.

When I check the error logs I get the below error

{ Error: connect ECONNREFUSED
   at Object.exports._errnoException (util.js:1050:11)
   at exports._exceptionWithHostPort (util.js:1073:20)
   at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1093:14)
 syscall: 'connect',
 address: '',
 port: 2368,
  { adapter: [Function: httpAdapter],
    transformRequest: { '0': [Function: transformRequest] },
    transformResponse: { '0': [Function: transformResponse] },
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    validateStatus: [Function: validateStatus],
     { Accept: 'application/json, text/plain, */*',
       Authorization: 'Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVjODU1ZTAwYmM1MzNlMDAwMWJmZjkwNyJ9.eyJpYXQiOjE1NTI0MTQ5NjIsImV4cCI6MTU1MjQxNTI2MiwiYXVkIjoiL3YyL2FkbWluLyJ9.3JRLTL-xr7qWAdwTGakwcOQL114_9eSgn-im1V3FeX8',
       'User-Agent': 'axios/0.18.0' },
    method: 'get',
    url: 'http://localhost:2368/ghost/api/v2/admin/posts/?limit=1',
    data: undefined },
  Writable {
     WritableState {
       objectMode: false,
       highWaterMark: 16384,
       needDrain: false,
       ending: false,
       ended: false,
       finished: false,
       decodeStrings: true,
       defaultEncoding: 'utf8',
       length: 0,
       writing: false,
       corked: 0,
       sync: true,
       bufferProcessing: false,
       onwrite: [Function: bound onwrite],
       writecb: null,
       writelen: 0,
       bufferedRequest: null,
       lastBufferedRequest: null,
       pendingcb: 0,
       prefinished: false,
       errorEmitted: false,
       bufferedRequestCount: 0,
       corkedRequestsFree: [Object] },
    writable: true,
    domain: null,
     { response: [Function: handleResponse],
       error: [Function: handleRequestError] },
    _eventsCount: 2,
    _maxListeners: undefined,
     { maxRedirects: 21,
       maxBodyLength: 10485760,
       protocol: 'http:',
       path: '/ghost/api/v2/admin/posts/?limit=1',
       method: 'get',
       headers: [Object],
       agent: undefined,
       auth: undefined,
       hostname: 'localhost',
       port: '2368',
       nativeProtocols: [Object],
       pathname: '/ghost/api/v2/admin/posts/',
       search: '?limit=1' },
    _ended: true,
    _ending: true,
    _redirectCount: 0,
    _redirects: [],
    _requestBodyLength: 0,
    _requestBodyBuffers: [],
    _onNativeResponse: [Function],
     ClientRequest {
       domain: null,
       _events: [Object],
       _eventsCount: 6,
       _maxListeners: undefined,
       output: [],
       outputEncodings: [],
       outputCallbacks: [],
       outputSize: 0,
       writable: true,
       _last: true,
       upgrading: false,
       chunkedEncoding: false,
       shouldKeepAlive: false,
       useChunkedEncodingByDefault: false,
       sendDate: false,
       _removedHeader: {},
       _contentLength: 0,
       _hasBody: true,
       _trailer: '',
       finished: true,
       _headerSent: true,
       socket: [Object],
       connection: [Object],
       _header: 'GET /ghost/api/v2/admin/posts/?limit=1 HTTP/1.1\r\nAccept: application/json, text/plain, */*\r\nAuthorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVjODU1ZTAwYmM1MzNlMDAwMWJmZjkwNyJ9.eyJpYXQiOjE1NTI0MTQ5NjIsImV4cCI6MTU1MjQxNTI2MiwiYXVkIjoiL3YyL2FkbWluLyJ9.3JRLTL-xr7qWAdwTGakwcOQL114_9eSgn-im1V3FeX8\r\nUser-Agent: axios/0.18.0\r\nHost: localhost:2368\r\nConnection: close\r\n\r\n',
       _headers: [Object],
       _headerNames: [Object],
       _onPendingData: null,
       agent: [Object],
       socketPath: undefined,
       timeout: undefined,
       method: 'GET',
       path: '/ghost/api/v2/admin/posts/?limit=1',
       _ended: false,
       res: null,
       aborted: undefined,
       timeoutCb: null,
       upgradeOrConnect: false,
       parser: null,
       maxHeadersCount: null,
       _redirectable: [Circular] },
    _currentUrl: 'http://localhost:2368/ghost/api/v2/admin/posts/?limit=1' },
 response: undefined }

If I curl run below, its works as a bash command

curl -H "Authorization: 'Ghost $token'" http://localhost:2368/ghost/api/v2/admin/posts/?limit=1

@Extracreative it sounds like the docker instance you’re running your express app inside doesn’t have network access to your Ghost instance. Are you running the curl command inside your node docker container?

What you’re seeing isn’t directly connected to Ghost as Ghost won’t drop or refuse connections, it would be responding with an API response detailing what’s wrong.


Your right, I should have been using host.docker.internal:2368 instead of localhost:2368 as I’m running docker-compose on my Mac.

@Kevin Thanks for your help


I’m still curious why you’re doing things the hard way instead of using the API client?


@Hannah I have ghost running in a docker container. I have a database of data which I need the Ghost Blog to ingest data and create articles. Therefore I’m using the express database to pull the data from another source and post it into the Admin API endpoint to create the articles. The issue I was facing was the containers weren’t communicating with each other on a Mac.


:confused: I’m asking why you chose to write your own code instead of using this: https://docs.ghost.org/api/javascript/admin/


@Hannah Oh yeah, I am going to.


@Hannah I use my own code just to check if I’d get the same error

closed #13

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.