INVALID_JWT error in Google Apps Script

Hey team :wave:t4:

I’m trying to add members to my Ghost (Pro) account via the Admin API but keep running into the following error —

{
  "errors": [
    {
      "message": "Invalid token: invalid signature",
      "context": null,
      "type": "UnauthorizedError",
      "details": null,
      "property": null,
      "help": null,
      "code": "INVALID_JWT",
      "id": "d3271f10-be6a-11ec-a615-c128442a4338"
    }
  ]
}

I was able to successfully run the API on Node.js so I know that all my credentials, endpoint, POST payload etc. are in the correct format.

Here’s the code but you can just as easily make a copy of my Google Apps Script project instead —

const accessToken = `<replace-this-with-your-access-token>`;
const admin_domain = `<replace-this-with-your-admin-domain>`;
let [id, secret] = accessToken.split(':');

function addMemberToGhost() {
  let options = {
    'method': 'POST',
    'headers': {
      'Authorization': `Ghost ${createJwt()}`, // https://ghost.org/docs/admin-api/#token-authentication
    },
    'muteHttpExceptions': true,
    'contentType': 'application/json',
    'payload': JSON.stringify({
      "members": [
        {
          "email": "sourabh@choraria.io",
          "name": "Sourabh Choraria",
        }
      ]
    }),
  }
  const response = UrlFetchApp.fetch(`https://${admin_domain}/ghost/api/v3/admin/members/`, options);
  console.log(response.getResponseCode());
  console.log(JSON.stringify(JSON.parse(response.getContentText()), null, 2));
}

function createJwt() { // adopted from https://www.labnol.org/code/json-web-token-201128
  const header = Utilities.base64EncodeWebSafe(JSON.stringify({
    alg: 'HS256',
    kid: id,
    typ: 'JWT'
  })).replace(/=+$/, '');

  const now = Date.now();
  let expires = new Date(now);
  expires.setMinutes(expires.getMinutes() + 5);

  const payload = Utilities.base64EncodeWebSafe(JSON.stringify({
    exp: Math.round(expires.getTime() / 1000),
    iat: Math.round(now / 1000),
    aud: '/v3/admin/'
  })).replace(/=+$/, '');

  // https://gist.github.com/tanaikech/707b2cd2705f665a11b1ceb2febae91e#sample-script
  // Convert hex 'secret' to byte array then base64Encode
  secret = Utilities.base64EncodeWebSafe(secret
  .match(/.{2}/g)
  .map((e) =>
    parseInt(e[0], 16).toString(2).length == 4
      ? parseInt(e, 16) - 256
      : parseInt(e, 16)
  )).replace(/=+$/, '');

  const toSign = `${header}.${payload}`;
  const signatureBytes = Utilities.computeHmacSha256Signature(toSign, secret);
  const signature = Utilities.base64EncodeWebSafe(signatureBytes).replace(/=+$/, '');
  const jwt = `${toSign}.${signature}`;
  console.log({jwt});
  return jwt;
};

I think my issues are with trying to convert hex secret to byte array (line 45) part of the code but I could be wrong :thinking:

Any help would be greatly appreciated :pray:t4:

Thanks to Riël Notermans, fixed it by modifying the createJwt function like so —

function createJwt() { // adopted from https://www.labnol.org/code/json-web-token-201128
  const header = Utilities.base64EncodeWebSafe(JSON.stringify({
    alg: 'HS256',
    kid: id,
    typ: 'JWT'
  })).replace(/=+$/, '');

  const now = Date.now();
  let expires = new Date(now);
  expires.setMinutes(expires.getMinutes() + 5);

  const payload = Utilities.base64EncodeWebSafe(JSON.stringify({
    exp: Math.round(expires.getTime() / 1000),
    iat: Math.round(now / 1000),
    aud: '/v3/admin/'
  })).replace(/=+$/, '');

  // https://gist.github.com/tanaikech/707b2cd2705f665a11b1ceb2febae91e#sample-script
  // Convert hex 'secret' to byte array then base64Encode
  secret = secret
  .match(/.{2}/g)
  .map((e) =>
    parseInt(e[0], 16).toString(2).length == 4
      ? parseInt(e, 16) - 256
      : parseInt(e, 16)
  );

  const toSign = Utilities.newBlob(`${header}.${payload}`).getBytes();
  const signatureBytes = Utilities.computeHmacSha256Signature(toSign, secret);
  const signature = Utilities.base64EncodeWebSafe(signatureBytes).replace(/=+$/, '');
  const jwt = `${header}.${payload}.${signature}`;
  console.log({jwt});
  return jwt;
};
2 Likes

Two years later, but I must comment to say that this code saved my podcast automation in n8n. This was the only code that worked to create a JWT token to create a post. I was tearing my hair out and this finally did it. Thanks so much.