Extension Chat Message returns 401 authentication failed

Hi, When a viewer used my extension , extension should send a message to chat about that user in that channel. Just want to send a simple message.

I’m working on “send extension chat message” also i tried “Send Extension PubSub Message” too. When i tried pubsub i was getting “403 - JWT could not be verified” error and i cant figure out it. Now i’m trying other way but i’m getting “401 authentication failed” error.
I have read a lot of explanations and posts on this subject and tried a lot of things. I am very confused now.

EBS is writen in PHP.

$payload_arr = array(
“exp” => $time,
“user_id”=> $channelid,
“role”=> “external”
);

$payload = json_encode($payload_arr);
// {“exp”:1592921206,“user_id”:“49354541”,“role”:“external”}

$jwt = JWT::encode($payload, $secretkey);

// I also check the token at jwt.io too.

//Send Extension Chat Message

$url = “https://api.twitch.tv/extensions/“.$clientid.”/“.$clientversion.”/channels/“.$channelid.”/chat”;
$headers = array(
"Authorization: Bearer ".(string)$jwt,
"Client-Id: ".$clientid,
“Content-Type: application/json”
);

$data = array(“text” => ‘Extension send a message to chat’);

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_exec($ch);
$dresp = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);

Request Header:

[request_header] => POST /extensions/89sl308o9vzckjsz2viv6mx76ihask/1.1.1/channels/49354541/chat HTTP/2
Host: api.twitch.tv
accept: /
authorization: Bearer [token]
client-id: 89sl308o9vzckjsz2viv6mx76ihask
content-type: application/json
content-length: 37

Response;

{“error”:“Unauthorized”,“status”:401,“message”:“authentication failed”}

What am i doing wrong. Thanks for help.

Is the extension of the same version installed and active on the channel?
Has the streamer got the chat option enabled?

image

https://dashboard.twitch.tv/u/STREAMERNAME/extensions/permissions

Yes it’is installed on my channel for testing and also chat options enabled.

Oh you did base64 decode your extension secret before using it to encode the JWT?

The only thing I do different is for pubsub I use, (javascript but easy enough to convert to PHP)

const sigPubSubPayload = {
    "exp": Math.floor(new Date().getTime() / 1000) + 60,
    "user_id": config.owner,
    "role": "external",
    "channel_id": "all",
    "pubsub_perms": {
        "send": [
            "global"
        ]
    }
}

And chat

    var sigPayload = {
        'exp':          Math.floor(new Date().getTime() / 1000) + 60,
        'user_id':      config.twitch.streamer_id,
        'role':         'broadcaster'
    }

Since you’ve tried both PubSub and chat, and got the same issue, I bet you forgot (or didn’t know) to base decode your secret before passing it to the encoder

This is the class i used for JWT encode.
Also i copied jwt code and paste to jwt.io for check payload data. On that site its decoded like this.

{"exp":1592923080,"user_id":"49354541","role":"external"}"

so it’ll be about base64 encode/decode. Going to check that.

public static function encode($payload, $key, $alg = ‘HS256’, $keyId = null, $head = null)
{
$header = array(‘typ’ => ‘JWT’, ‘alg’ => $alg);

    if ($keyId !== null) {
        $header['kid'] = $keyId;
    }

    if ( isset($head) && is_array($head) ) {
        $header = array_merge($head, $header);
    }

    $segments = array();
    $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
    $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
    $signing_input = implode('.', $segments);
    $signature = static::sign($signing_input, $key, $alg);
    $segments[] = static::urlsafeB64Encode($signature);

    return implode('.', $segments);
}
public static function sign($msg, $key, $alg = 'HS256')
{
    if (empty(static::$supported_algs[$alg])) {
        throw new Exception('Algorithm not supported');
    }
    list($function, $algorithm) = static::$supported_algs[$alg];
    switch($function) {
        case 'hash_hmac':
            return hash_hmac($algorithm, $msg, $key, true);
        case 'openssl':
            $signature = '';
            $success = openssl_sign($msg, $signature, $key, $algorithm);
            if (!$success) {
                throw new Exception("OpenSSL unable to sign data");
            } else {
                return $signature;
            }
    }
}

I’m using extension secret at extension client configuration.

I’ll delete key.
$base64key=“REMOVED=”; // base64 encoded key
$decodedkey = base64_decode($base64key);
printed : �����,qE�-��5�٭�2Q?"'z��7

base64 decode returns key with invalid characters. I created new extension secret but it returns invalid too.

Also I’m getting 401 error with that undecoded key too.

Removed your key as it counts as a password and shouldn’t be posted publically.

Try this instead, confirmed as working having just tested it, (it’s a bad/old/dumb test script for the chat endpoint I have kicking about in PHP)

For this $user_id should be the ID of the channel that you want to send Chat messages to.

<?php

echo date('r', time());
echo "\n";

$client_id = 'CLIENID';
$secret = base64_decode('SECRET');
$version = '0.0.1';
$user_id = 'CHANNEL_ID';

// end edits

include(__DIR__ . '/jwt.php');
$j = new JWT();

$payload = array(
    'exp' => time() + 60,
    'user_id' => ''.$user_id,
    'role' => 'broadcaster'
);
$bearer = $j->encode($payload, $secret);

echo "\n" . $bearer . "\n";

$data_string = json_encode(array(
    'text' => 'This is a test'
));

$url = 'https://api.twitch.tv/extensions/' . $client_id . '/' . $version . '/channels/' . $user_id . '/chat';

echo $url;

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_ENCODING , "gzip");
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Authorization: Bearer ' . $bearer,
    'Client-ID: ' . $client_id,
    'Content-Type: application/json'
));
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);

$r = curl_exec($ch);
$i = curl_getinfo($ch);

curl_close($ch);

echo "\n" . $i['http_code'] . "\n";

echo "\n";
echo $r;
echo "\n";

jwt.php is from https://github.com/firebase/php-jwt/blob/master/src/JWT.php

But my current version of the file is, I only tested against my version as I know it works

Most notably is my payload differs

$payload = array(
    'exp' => time() + 60,
    'user_id' => ''.$user_id,
    'role' => 'broadcaster'
);

However

$payload = array(
    'exp' => time() + 60,
    'user_id' => '15185913',
    'role' => 'external'
);

Also works (my user_id and I own the extension posting to another channel)

+60 for the exp is SUPER LIBERAL really too

I checked your code and compare with mine.

I found the problem. I was encode the payload with json before create JWT.

Now it works. Thanks for help.

1 Like

Welcome.

Probably also worth just using the JWT lib I linked rather than custom code, but if your stuff works all the better!

Hey, I am getting

{
    "error": "Unauthorized",
    "status": 401,
    "message": "authentication failed"
}

Do we need to create our own JWT to call this API? or can we use the auth token provided to us in onAuthorized method?

@BarryCarlyon

I believe only a broadcasters token is valid for sending to their own chat.

The documentation for the end point says you need to use an external JWT. So you should generate your own JWT

Recommends the usage of an external

Oh okay, And the secret to sign this JWT will be the client secret right?

No.

It’s the Extension Secret, not the Twitch API Client Secret

This is the one right?

I am using this code to generate the Jwt to call the api but still getting the same error.

const sec = "secret from above image";
const secret = Buffer.from(sec, "base64");
app.post("/getJWTToken", (req, res) => {
  let data = {
    channel_id: 768266494,
    role: "external",
  };

  const token = jsonwebtoken.sign(data, secret);
  res.send(token);
});

You need to cast the channelID as a string not a number

So

const sigChatPayload = {
    "exp": Math.floor(new Date().getTime() / 1000) + 4,
    "user_id": "123123",
    "role": "external"
}
const sigChat = jwt.sign(sigChatPayload, ext_secret);

// We have now prepared the Signature and data

got({
    url: "https://api.twitch.tv/helix/extensions/chat",
    method: "POST",
    headers: {
        "Client-ID": config.client_id,
        "Authorization": "Bearer " + sigChat
    },
    json: {
        broadcaster_id: "321321",
        text: "This is a Test Message Kappa",
        extension_id: config.client_id,
        extension_version: config.extension_version
    },
    responseType: 'json'
})

Also

This seems dangerous. It looks like you are intending to leak generated JWT’s which is potentially dangerous to do so.

1 Like

Hey @BarryCarlyon I tried this

const sec = "mysecret_key";
const secret = Buffer.from(sec, "base64");
const bearerPrefix = "Bearer ";

const sigChatPayload = {
  exp: Math.floor(new Date().getTime() / 1000) + 4,
  channel_id: "152826156",
  role: "external",
};

const sigChat = jsonwebtoken.sign(sigChatPayload, secret);
const client_id = "y7aqa290cuap0tqrvs44t3wr9qu20f";
got({
  url: "https://api.twitch.tv/helix/extensions/chat",
  method: "POST",
  headers: {
    "Client-ID": client_id,
    Authorization: "Bearer " + sigChat,
  },
  json: {
    broadcaster_id: 152826156,
    text: "This is a Test Message Kappa",
    extension_id: client_id,
    extension_version: "0.0.1",
  },
  responseType: "json",
})
  .then((resp) => {
    // console log out the useful information
    // keeping track of rate limits is important
    // you can only set the config 12 times a minute per segment
    console.error(
      "Send Chat OK",
      resp.statusCode,
      resp.headers["ratelimit-remaining"],
      "/",
      resp.headers["ratelimit-limit"]
    );

    // we don't care too much about the statusCode here
    // but you should test it for a 204
  })
  .catch((err) => {
    if (err.response) {
      console.error("Errored", err.response.statusCode, err.response.body);
      return;
    }
    console.error(err);
  });

But still getting

I am live on the broadcaster channel and have the extension installed there as well

You did the the extension secret and not the extension client secret

The bottom one:

For ease of testing lets also change

const sigChatPayload = {
  exp: Math.floor(new Date().getTime() / 1000) + 4,
  channel_id: "152826156",
  role: "external",
};

to

const sigChatPayload = {
  exp: Math.floor(new Date().getTime() / 1000) + 4,
  user_id: "The ID of the Twitch Account that owns the extension",
  role: "external",
};

and the broadcaster_id in the payload is not correct

  json: {
    broadcaster_id: 152826156,
    text: "This is a Test Message Kappa",
    extension_id: client_id,
    extension_version: "0.0.1",
  },

should be a string

  json: {
    broadcaster_id: "152826156",
    text: "This is a Test Message Kappa",
    extension_id: client_id,
    extension_version: "0.0.1",
  },

Also check the version numbers match. That 0.0.1 is active on the destination channel

  • I am using Extension Client Configuration
  • changed the channel_id to user_id and set it to the id of the twitch account that owns the extension
  • Made the broadcaster_id value to be a string

Still getting the same error

The extension does have the capability enabled:

And “Chat” is enabled in the streamer permissions

image

This is on Creator Dashboard then click My Extensions then Manage Permissions alternativelly uninstall and reinstall the extension (not unactivate and reactivate)

1 Like

Thank you @BarryCarlyon . It works now.