403 JWT Could not be verified - EBS PubSub

I’m trying to sign an EBS JWT, and send a PubSub message to all viewers on a channel that has my Extension. When I call twitch’s API to send the PubSub message, I get back “403 JWT Could not be verified”, so I know I must be signing it wrong.

I’m using .NET Core (aka: dotnet core), but I have to admit, I’ve never had to build and sign JWT’s before, so there’s likely an issue w/ my code (below) or maybe I’m using the wrong encryption algorithm (HS256):

private string generateEbsJwt(string channelId)
    var claims = new[]
        new Claim("channel_id", channelId),
        new Claim("user_id", channelId),
        new Claim("role", "external"),
        new Claim("pubsub_perms", JsonConvert.SerializeObject( new { send = new[] {  "broadcast" } })),

    string twitchExtensionSecret = _configuration.GetValue<string>("TwitchExtSecret");

    var token = new JwtSecurityToken
        claims: claims,
        expires: DateTime.UtcNow.AddHours(10),
        signingCredentials: new SigningCredentials( new SymmetricSecurityKey(Convert.FromBase64String(twitchExtensionSecret)),

    var jwtHandler = new JwtSecurityTokenHandler();
    var signedToken = jwtHandler.WriteToken(token);

    return signedToken;

The “TwitchExtSecret” value that I’m reading from configuration is the Secret I obtained from the dev.twtich.tv console for my extension, as-is without changing it.
So that value feeds into
new SymmetricSecurityKey(Convert.FromBase64String(twitchExtensionSecret)
where it’s considered a base 64 string and that line of code converts it to a byte[]. Am I doing that wrong?

Also, this is how i’m sending the actual request to Twitch, including adding the Bearer token as an Authorization header:

var authorizationHeader = generateEbsJwt(channelId);

var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.twitch.tv/extensions/message/{channelId}");
request.Content = new StringContent(sendMessageContent);
request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
request.Headers.Add("Authorization", "Bearer " + authorizationHeader);
request.Headers.Add("Client-Id", _configuration.GetValue<string>("TwitchExtClientId"));

var response = await httpClient.SendAsync(request);

This is my C# code, I use a NuGet package called JWT, I found it easier to use than the built in

   private string GetAuthToken()
        var payload = new Dictionary<string, object>
            { "exp", GetEpoch() + 80 },
            { "opaque_user_id", $"all" },
            { "user_id", "all" },
            { "channel_id", "all" },
            { "role", "broadcaster"},

        IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
        IJsonSerializer serializer = new JsonNetSerializer();
        IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
        IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);

        var token = encoder.Encode(payload, Convert.FromBase64String(_extension.Secret));
        return token;
    private int GetEpoch()
        TimeSpan t = DateTime.UtcNow - new DateTime(1970, 1, 1);
        int secondsSinceEpoch = (int)t.TotalSeconds;
        return secondsSinceEpoch;

Thanks, I’ve swapped in your code and installed the JWT NuGet library (also added a “pubsub_perms” Claim, and changed the “role” Claim to “external” per the docs for pubsub)…

But I’m still getting back the same HTTP response from Twitch:
{“error”:“Forbidden”,“status”:403,“message”:“Error (403): JWT could not be verified”}

I don’t get what I could be doing wrong… :frowning: I am using the same Extension Secret value in my EBS to validate incoming Extension viewer JWT’s on other endpoints… so I don’t think that could be the problem. I may try generating a new Secret anyway

just a shot in the dark but try

request.Headers.Authorization = new AuthenticationHeaderValue(“Bearer”, authorizationHeader);

instead of your code to add the header

Thanks Syzuna - just tried that, but getting the same 403 result.
One other thing I thought of: maybe the channel needed to be live, for me to test calling this PubSub API endpoint… but just tried starting a stream and calling the endpoint, same error.

Are you sure you’re using the correct secret?

The Extension Console shows 2 secrets, one under the Twitch API Client Configuration section, which is just for API usage, and is NOT for signing/verifying JWTs.

The correct extension secret for signing/verifying JWTs is in the Extension Client Configuration section.

hi @Dist, yep I’ve got the secret from the “Extension Client Configuration” section.

i just tried another thing, which gave me more confidence that i’m using the right Secret:
i tried calling this Get Extension Secret endpoint, using the same C# code to build my own EBS JWT (but with slightly different Claims / JWT payload)… and this endpoint is working! but the PubSub one still isn’t!

PubSub is driving me nuts. Could it be something like: I need to configure my channel to allow PubSub? Like when i coded my first Bits Transaction and tried like heck to test it for days, then learned i couldn’t test it live on my channel because i wasn’t an affiliate/partner… anything like that relevant to PubSub?

Extension PubSub is always enabled. I’m not a .NET dev so can’t help with your code, but what I’d suggest doing is just logging All The Things!

If you log your signed JWT you can easily verify that the payload data is correct, and if you log the secret being used (before being base64 decoded) you can check to see if there are any additional spaces or missing characters.

If you paste your generated JWT into something like jwt.io and use your secret directly copy/pasted from your Extension Console page then that should correctly show the signature is correct, if it shows the signature is incorrect then it’s likely an issue with how your EBS is loading/manipulating the secret string or how it’s signing the JWT.

Okay, JWT, this can be fun with PubSub, I wrote a post a while back somewhere. Give me an hour or two as I’m just doing something.

I’ll find the right stuff and post it


const broadcast = {
  "exp": Math.round(Date.now() / 1000) + 60,
  "user_id": "14900522",
  "role": "external",
  "channel_id": "{{the channel to broadcast to}}",
  "pubsub_perms": {
const global = {
  "exp": Math.round(Date.now() / 1000) + 60,
  "user_id": "14900522",
  "role": "external",
  "channel_id": "all",
  "pubsub_perms": {
const whisper = {
  "exp": Math.round(Date.now() / 1000) + 60,
  "user_id": "14900522",
  "role": "external",
  "channel_id": "{{the channel that the user is on}}",
  "pubsub_perms": {
1 Like

@LuckyNoS7evin thanks to you and Dist. I did use jwt.io the other day to analyze the Bearer token i’m sending, but you guys prompted me to look at it again.

I noticed that my “pubsub_perms” claim value just seemed… weird. i looked at my code again and saw that i was constructing the “pubsub_perms” object with the “send”: [ “broadcast” ] property… but all the strings were escaped (\") because i serialized the Claim value to JSON when i set it in my EBS.

i took that out, now setting the Claim value as an object, instead of the object serialized to a JSON string… and that fixed it!

thanks, all!