JWTs are giving me headaches - signature format wrong?

So, let’s say that I wanted to generate JWTs client side, not on the server. Yes, there are security issues. I know. I’m NOT actually storing the API key client side, which I think addresses the issue.

DISCLAIMER: All the API keys posted below are for a localhost test environment that’s not routable beyond my home router. Never post your real API keys anywhere. :slight_smile:

I’m hitting a snag that looks like a problem with JWT generation and what’s supposed to be in the signature. I checked to see if my JWTs validated on jwt.io, and they do, but Ghost doesn’t like them, returning:

"code":"INVALID_JWT","name":"UnauthorizedError","statusCode":401,"level":"normal","message":"Invalid token: invalid signature","stack":"JsonWebTokenError: invalid signature
   at authenticateWithToken (D:\\Programming\\client sites\\theme_edits\\versions\\5.26.2\\core\\server\\services\\auth\\api-key\\admin.js:163:29)
    at D:\\Programming\\client sites\\theme_edits\\versions\\5.26.2\\node_modules\\jsonwebtoken\\verify.js:133:19
at getSecret (D:\\Programming\\client sites\\theme_edits\\versions\\5.26.2\\node_modules\\jsonwebtoken\\verify.js:90:14)
at Object.module.exports [as verify] (D:\\Programming\\client sites\\theme_edits\\versions\\5.26.2\\node_modules\\jsonwebtoken\\verify.js:94:10)
at authenticateWithToken (D:\\Programming\\client sites\\theme_edits\\versions\\5.26.2\\core\\server\\services\\auth\\api-key\\admin.js:160:17)

I’ve tried base64 encoding and not base64 encoding the signature. It doesn’t seem to matter. I get 401 Unauthorized either way.

Here’s an example JWT-containing header:

Authorization: Ghost eyJhbGciOiJIUzI1NiIsImtpZCI6IjYzNzk4NmY0ODY2MGM4MTY0MDBlYWVlNCIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzMxMDg0ODEsImlhdCI6MTY3MzEwODE4MSwiYXVkIjoiL2FkbWluLyJ9.y4rrPlxHabmD_dfdqGasE6-Q860QJ5GvQ1PvVaNfdqE

Here’s what jwt.io says:

My API key, if anyone would like to check my work:

Help would be appreciated. I’m at about three hours in and am about ready to pull my hair out on this one.

The code I’m running client side:

<script type="text/javascript" src="/assets/js/crypto-js/crypto-js.js"></script>

function base64url(source) {
  // Encode in classical base64
  encodedSource = CryptoJS.enc.Base64.stringify(source);

  // Remove padding equal characters
  encodedSource = encodedSource.replace(/=+$/, '');

  // Replace characters according to base64url specifications
  encodedSource = encodedSource.replace(/\+/g, '-');
  encodedSource = encodedSource.replace(/\//g, '_');

  return encodedSource;

function makeKey(key) {
const [id, secret] = key.split(':');

var header = {
    "alg": "HS256",
    "kid": id, 
    "typ": "JWT"

var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);

var data = 
    "exp": Math.floor(Date.now() / 1000) + 300 , 
    "iat": Math.floor(Date.now() / 1000), 
    "aud": "/admin/"

var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
var encodedData = base64url(stringifiedData);

var token = encodedHeader + "." + encodedData;
console.log('secret is',secret)
var signature = CryptoJS.HmacSHA256(token, secret);
signature = base64url(signature);

var signedToken = token + "." + signature;
return signedToken


I didn’t solve this problem directly today.

Instead, I discovered that the Admin API SDK will run with browserify. So fine. It can generate the jwts for me, then.

In other (but closely related) news, my CORS issue with sites with admin and front end on different domains (including many Ghost Pro sites) are fixed as a result of the switch to JWTs!

1 Like