tsndr / cloudflare-worker-jwt

A lightweight JWT implementation with ZERO dependencies for Cloudflare Workers.
MIT License
649 stars 51 forks source link

Maybe an error #35

Closed gnimmelf closed 1 year ago

gnimmelf commented 1 year ago

Hi,

sorry to poke you with this, but I'm at a dead end:

I have a CF worker that needs a JWT to access a Ghost site (v5.15) admin-api. For a week now I have been trying to make that work, but regardless of what I try, I cannot get it working.

I have this issue over at the ghost forum: https://forum.ghost.org/t/cloudflare-worker-jwt/37729 with a copy of the worker script that might shed some light on this.

I have a node-script using jsonwebtoken with same setup to double-check verification & debugging, and that succeeds.

After numerous videos and online tutorials on JWT, this is my list of possible reasons:

  1. My poor understanding: I just need to encode the secret some way I haven't tried
  2. The Ghost version is borked, but I can't find anything indicating this
  3. There is a difference in the encoding of the secret on it's way to hashing between jsonwebtoken and this repo

-Any pointer on where to look?

Thanks!

tsndr commented 1 year ago

Hi, I did some testing and can you please try this:

import jwt from '@tsndr/cloudflare-worker-jwt';

function hexToUtf8(hex: string): string {
  return decodeURIComponent(hex.replace(/\s+/g, '').replace(/[0-9a-f]{2}/g, '%$&'));
}

export default {
  async fetch(request: Request, env: any) {
    // Admin API key goes here
    const API_KEY =
      'XXXXXXXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

    const [keyId, secret] = API_KEY.split(':');

    const nowTs = Math.round(Date.now() / 1000);

    const payload = {
      exp: nowTs + 300,
      iat: nowTs,
      aud: '/admin/',
    };

    const token = await jwt.sign(payload, hexToUtf8(secret), {
      algorithm: 'HS256',
      header: { typ: 'JWT', kid: keyId },
    });

    const data = await fetch(
      'https://example.com/ghost/api/admin/members',
      {
        headers: {
          Authorization: `Ghost ${token}`,
          'Content-Type': 'application/json',
          'Accept-Version': 'v5.15',
        },
        method: 'GET',
      }
    ).then(async (res) => {
      return {
        payload,
        token,
        res: await res.json(),
      };
    });

    return new Response(JSON.stringify(data, null, 2));
  },
};
gnimmelf commented 1 year ago

Hi,

thanks for taking the time.

I tried your suggestion, but alas, no success. Secrets generated from the ghost api won't conform:

URIError: URI malformed
    at decodeURIComponent (<anonymous>)
    at hexToUtf8

The funny thing is, I can't get the jsonwebtoken generated token (which succeeds towards the api) to verify on https://jwt.io/ , with or without "secret base64 encoded" checked. But tokens from this repo verifies.

Today i also tried it vs a ghost@latest, but same result.

Really irksome issue, and solution is probably really simple...

Thanks.

tsndr commented 1 year ago

I'm sorry, but I don't really have a way to test or reproduce the issue on my side.

gnimmelf commented 1 year ago

No worries, I had to take another route (for now). Feel free to close the issue, or keep it open if others hit the same hurdle.

Thanks for looking into it.