[SOLVED] Send Extension PubSub Message with JWT Bearer

Since the docs say ID tokens cannot be refreshed, I need to use a JWT bearer instead.

The docs have an example of using an Token ID (which can’t be refreshed).

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDMzNDM5NDcsInVzZXJfaWQiOiIyNzQxOTAxMSIsImNoYW5uZWxfaWQiOiIyNzQxOTAxMSIsInJvbGUiOiJleHRlcm5hbCIsInB1YnN1Yl9wZXJtcyI6eyJzZW5kIjpbIioiXX19.TiDAzrq58XczdymAozwsdVilRkjr9KN8C0pCv7px-FM" \
-H "Client-Id: pxifeyz7vxk9v6yb202nq4cwsnsp1t" \
-H "Content-Type: application/json" \
-d '{"content_type":"application/json", "message":"{\"foo\":\"bar\"}", "targets":["broadcast"]}' \
-X POST https://api.twitch.tv/extensions/message/27419011

You can keep track of the expire seconds, but once the Token ID expires, you have to open a webpage to reauthorize the user. This would be terribly inconvenient in the middle of a broadcast.

I’m already refreshing tokens in my extension backend. But I need to be able to use JWT to send messages, without interrupting the user.

Send your signed JWT in the request header, following this format:

Authorization: Bearer <signed JWT>

JWT signing and validation libraries are available for many languages at

I’m looking for an example.

Thanks!

Still getting 403s…

{
    "error": "Forbidden",
    "status": 403,
    "message": "{\n  \"status\": 403,\n  \"message\": \"JWT could not be verified\",\n  \"error\": \"Forbidden\"\n}"
}

C#

using System.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;

...

        public static long ToUnixTime(DateTime date)
        {
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return Convert.ToInt64((date - epoch).TotalSeconds);
        }

        private string GetSignedJWT()
        {
            DateTime now = DateTime.Now;
            DateTime expires = now + TimeSpan.FromSeconds(60);
            long exp = ToUnixTime(expires);

            var claimsIdentity = new ClaimsIdentity(new List<Claim>()
            {
                new Claim(ClaimTypes.NameIdentifier, _mUserId),
                new Claim(ClaimTypes.Role, "broadcaster"),
            }, "Custom");

            var plainTextSecurityKey = VALUE_CLIENT_SECRET;
            byte[] keyBytes = Encoding.UTF8.GetBytes(plainTextSecurityKey);

            var signingKey =
                new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(keyBytes);
            var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
                signingKey,
                SecurityAlgorithms.HmacSha256Signature);

            var securityTokenDescriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor()
            {
                Issuer = "Twitch",
                Subject = claimsIdentity,
                Audience = "OAuth2",
                Expires = expires,
                NotBefore = now,
                IssuedAt = now,
                SigningCredentials = signingCredentials
            };

            var jwtHeader = new JwtHeader(signingCredentials);

            JObject jsonPayload = new JObject();
            jsonPayload.Add("exp", exp);
            jsonPayload.Add("channel_id", _mUserId);
            jsonPayload.Add("user_id", _mUserId);
            jsonPayload.Add("role", "external");
            JObject pubsubPerms = new JObject();
            JArray send = new JArray();
            send.Add("*");
            pubsubPerms.Add("send", send);
            jsonPayload.Add("pubsub_perms", pubsubPerms);
            string payload = jsonPayload.ToString();
            JwtPayload jwtPayload = JwtPayload.Deserialize(payload);

            var secToken = new JwtSecurityToken(jwtHeader, jwtPayload);

            var tokenHandler = new JwtSecurityTokenHandler();
            var signedAndEncodedToken = tokenHandler.WriteToken(secToken);

            return signedAndEncodedToken;
        }

Given:

var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
                signingKey,
                SecurityAlgorithms.HmacSha256Signature);

The JWT is listing the alg as:

{[alg, http://www.w3.org/2001/04/xmldsig-more#hmac-sha256]}

The sample public key shows RS256.
https://api.twitch.tv/api/oidc/keys

Is this a problem?

I tried base64 decode the client_secret before use. No luck.

//byte[] keyBytes = Encoding.UTF8.GetBytes(VALUE_CLIENT_SECRET);

            string str = VALUE_CLIENT_SECRET;
            int mod4 = str.Length % 4;
            if (mod4 > 0)
            {
                str += new string('=', 4 - mod4);
            }
            byte[] keyBytes = Convert.FromBase64String(str);
{
    "error": "Forbidden",
    "status": 403,
    "message": "{\n  \"status\": 403,\n  \"message\": \"JWT has expired\",\n  \"error\": \"Forbidden\"\n}"
}

It helps using the EBS secret instead of the client secret.

Now to find out why it expired.

Success. I had to put the exp like 2 days in the future and now I can perhaps send messages.

Here is the working C# JWT generation code.

        public static long ToUnixTime(DateTime date)
        {
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return Convert.ToInt64((date - epoch).TotalSeconds);
        }

        private string GetSignedJWT()
        {
            DateTime now = DateTime.Now;
            DateTime expires = now + TimeSpan.FromDays(2);
            long exp = ToUnixTime(expires);

            var claimsIdentity = new ClaimsIdentity(new List<Claim>()
            {
                new Claim(ClaimTypes.NameIdentifier, _mUserId),
                new Claim(ClaimTypes.Role, "external"),
            }, "Custom");

            string str = VALUE_BACKEND_SECRET;
            int mod4 = str.Length % 4;
            if (mod4 > 0)
            {
                str += new string('=', 4 - mod4);
            }
            byte[] keyBytes = Convert.FromBase64String(str);

            var signingKey =
                new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(keyBytes);
            var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
                signingKey,
                SecurityAlgorithms.HmacSha256Signature);

            var securityTokenDescriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor()
            {
                Issuer = "Twitch",
                Subject = claimsIdentity,
                Audience = "OAuth2",
                Expires = expires,
                NotBefore = now,
                IssuedAt = now,
                SigningCredentials = signingCredentials
            };

            var jwtHeader = new JwtHeader(signingCredentials);

            JObject jsonPayload = new JObject();
            jsonPayload.Add("exp", exp);
            jsonPayload.Add("channel_id", _mUserId);
            jsonPayload.Add("user_id", _mUserId);
            jsonPayload.Add("role", "external");
            JObject pubsubPerms = new JObject();
            JArray send = new JArray();
            send.Add("*");
            pubsubPerms.Add("send", send);
            jsonPayload.Add("pubsub_perms", pubsubPerms);
            string payload = jsonPayload.ToString();
            JwtPayload jwtPayload = JwtPayload.Deserialize(payload);

            var secToken = new JwtSecurityToken(jwtHeader, jwtPayload);
            var tokenHandler = new JwtSecurityTokenHandler();
            var signedAndEncodedToken = tokenHandler.WriteToken(secToken);

            return signedAndEncodedToken;
        }