Trying to post PubSub message, getting 403

I know there are a lot of posts about this and a lot of suggestions, but none of the posts I’ve read were able to solve the issue we are facing.

We are trying to broadcast a PubSub Message, using the example request from here:

So our JWT payload looks like this:

{
  "exp": 1605699340,
  "user_id": "529074695",
  "role": "external",
  "channel_id": "529074695",
  "pubsub_perms": {
"send":[
  "broadcast"
]
  }
}

We are signing that JWT Token with our extension client Secret which we are Base64 decoding.
We keep getting a 403 error no matter what we tried and we can’t figure out why.

Our curl request looks like this:

    curl --location --request POST 'https://api.twitch.tv/extensions/message/529074695' \
--header 'Client-ID: **EXTENSION_CLIENT_ID_HERE**' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer REMOVED' \
--data-raw '{
    "content_type": "application/json",
    "message": "{\"foo\":\"bar\"}",
    "targets": [
        "broadcast"
    ]
}'

Any ideas why we are hitting the wall here?
Thank you!

You censored your Client ID, which is public

But not your Generated authorization, which is private.

You censored the wrong thing.

I fixed your post for you

What is the body error returned?

Initially I don’t see anything wrong.

And you did use your Extension secret and not the Extension Client Secret

The bottom one?

Hello Barry,

I didn’t know that the Client Id is public.
Weird.
The token had a short expiration, but you are right I should had posted an expired one.
Thank you for addressing that.

As for the client Id, yes we’ve used the one you’ve highlighted.

What’s the body error returned?

I highlighted the secret… not the clientID

Just don’t post one at all saves getting into bad practice!

{
    "error": "Forbidden",
    "status": 403,
    "message": "Error (403): JWT could not be verified"
}

And yes this is the secret we’ve used to sign the JWT

Can we look at the code you are using to generate your JWT signature?

I’m generating it for test purposes on Jwt.io

Here’s a screenshot of it (blurred the signature and the secret)

And then testing it on Postman

Can’t say I’ve used either of those tools for testing purposes so I don’t know if those achieve the expected results.

Here’s my example

Granted this is for sending to “global” but it’s easy to modify for single channel

or if you want a channel example

        const sigPubSubPayload = {
            "exp": Math.floor(new Date().getTime() / 1000) + 60,
            "user_id": config.extension.owner_id,
            "role": "external",
            "channel_id": channel_id,
            "pubsub_perms": {
                "send": [
                    "broadcast"
                ]
            }
        }
        const sigPubSub = jwt.sign(sigPubSubPayload, config.extension.secret);

        got({
            url: 'https://api.twitch.tv/extensions/message/' + sigPubSubPayload.channel_id,
            method: 'POST',
            headers: {
                "Client-ID": config.extension.client_id,
                "Content-Type": "application/json",
                "Authorization": "Bearer " + sigPubSub
            },
            body: JSON.stringify({
                "message": JSON.stringify({
                    event: which,
                    data
                }),
                "content_type": "application/json",
                "targets": sigPubSubPayload.pubsub_perms.send
            }),
            gzip: true
        })
        .then(resp => {
            // Same story here with the rate limit its around 60 per minute per topic
            console.error('Relay PubSub OK', channel_id, resp.statusCode, resp.headers['ratelimit-ratelimitermessagesbychannel-remaining'], '/', resp.headers['ratelimit-ratelimitermessagesbychannel-limit']);
        })
        .catch(err => {
            if (err.response) {
                console.error('Error', err.statusCode, err.response.body);
            } else {
                console.error('Generic Error', err);
            }
        })

config.extension.secret is the base64 decoded secret

config.extension.secret = Buffer.from(config.extension.extension_secret, 'base64');

Edit: I did a test. generated a JWT via JWT.io and it posted successfully.

Final things to check

  • The extension is installed and active on the target channel?
  • You did copy/paste the whole secret and JWT.io didn’t report a “weak secret”?

The error doesn’t suggest these things, since it’s having and issue with your JWT as a whole

Test code:

const fs = require('fs');
const path = require('path');

const config = JSON.parse(fs.readFileSync(
    path.join(
        __dirname,
        'config.json'
    )
));

// prepare for use
config.extension.secret = Buffer.from(config.extension.extension_secret, 'base64');

const got = require('got');

var sigPubSub = 'FromJWTIO';

        got({
            url: 'https://api.twitch.tv/extensions/message/56410307',
            method: 'POST',
            headers: {
                "Client-ID": config.extension.client_id,
                "Content-Type": "application/json",
                "Authorization": "Bearer " + sigPubSub
            },
            body: JSON.stringify({
                "message": JSON.stringify({
                    event: 'test',
                    data: {}
                }),
                "content_type": "application/json",
                "targets": ['broadcast']
            }),
            gzip: true
        })
        .then(resp => {
            // Same story here with the rate limit its around 60 per minute per topic
            console.error('Relay PubSub OK', 'test', resp.statusCode, resp.headers['ratelimit-ratelimitermessagesbychannel-remaining'], '/', resp.headers['ratelimit-ratelimitermessagesbychannel-limit']);
        })
        .catch(err => {
            if (err.response) {
                console.error('Error', err.statusCode, err.response.body);
            } else {
                console.error('Generic Error', err);
            }
        })

As usual, the solution was simple but difficult to spot :persevere:

We were using the wrong Client-Id for the request
The simplest things are the hardest to figure out…

Thank you for your help Barry and apologies for wasting your time here.

s’all good

glad it was something simple.

1 Like