5

I'm trying to write a custom JWT token generator for C# .NET on Windows for use in Google Firebase. The source I lifted from a few different places here on StackOverflow (I'm trying to re-discover them so I can credit the original authors and ask a couple of additional questions too), so it's a bit of a hodge-podge together.

For the most part it seems to work, but it doesn't seem to generate a valid (firebase) token as expected. This could be entirely due to my lack of understanding of how it should work in the first place, but if I could get a second pair of world-class-awesome-engineers eyes from here to point out what I did wrong and make things work, it would be nice.

Here's the code:

// Token generator setup
using Newtonsoft.Json;
using System;
using System.IO;
using System.Web;
using System.Web.Configuration;

namespace myapp.Utils
{
    public class GoogleJsonWebToken
    {
        public static string Encode(string uid)
        {
            var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            var issueTime = DateTime.Now;

            var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
            var exp = (int)issueTime.AddMinutes(60).Subtract(utc0).TotalSeconds;

            var firebaseInfPath = HttpContext.Current.Server.MapPath(WebConfigurationManager.AppSettings["firebaseInf"]);
            var firebaseInfJsonContent = File.ReadAllText(firebaseInfPath);
            var firebaseInf = JsonConvert.DeserializeObject<dynamic>(firebaseInfJsonContent);
            var privateKey = (string)firebaseInf.private_key;

            var payload = new
            {
                iss = firebaseInf.client_email,
                scope = firebaseInf.client_email,
                aud = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
                exp = exp,
                uid = uid,
                iat = iat,
                claims = new { premium_account = true }
            };

            return myapp.Utils.JsonWebToken.Encode(payload, privateKey, JwtHashAlgorithm.RS256);
        }
    }
}

Here's the JWT hash algorithm code:

// JsonWebToken.cs
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

namespace myapp.Utils
{
    public enum JwtHashAlgorithm
    {
        RS256,
        HS384,
        HS512
    }

    public class JsonWebToken
    {
        private static Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>> HashAlgorithms;

        static JsonWebToken()
        {
            HashAlgorithms = new Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>>
            {
                { JwtHashAlgorithm.RS256, (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } },
                { JwtHashAlgorithm.HS384, (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } },
                { JwtHashAlgorithm.HS512, (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } }
            };
        }

        public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
        {
            var keyBytes = Encoding.UTF8.GetBytes(key);
            return Encode(payload, keyBytes, algorithm);
        }

        public static string Encode(object payload, byte[] keyBytes, JwtHashAlgorithm algorithm)
        {
            var segments = new List<string>();
            var header = new { alg = algorithm.ToString(), typ = "JWT" };

            byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
            byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));
            //byte[] payloadBytes = Encoding.UTF8.GetBytes(@"{"iss":"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com","scope":"https://www.googleapis.com/auth/prediction","aud":"https://accounts.google.com/o/oauth2/token","exp":1328554385,"iat":1328550785}");

            segments.Add(Base64UrlEncode(headerBytes));
            segments.Add(Base64UrlEncode(payloadBytes));

            var stringToSign = string.Join(".", segments.ToArray());

            var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);

            byte[] signature = HashAlgorithms[algorithm](keyBytes, bytesToSign);
            segments.Add(Base64UrlEncode(signature));

            return string.Join(".", segments.ToArray());
        }

        public static string Decode(string token, string key)
        {
            return Decode(token, key, true);
        }

        public static string Decode(string token, string key, bool verify)
        {
            var parts = token.Split('.');
            var header = parts[0];
            var payload = parts[1];
            byte[] crypto = Base64UrlDecode(parts[2]);

            var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
            var headerData = JObject.Parse(headerJson);
            var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
            var payloadData = JObject.Parse(payloadJson);

            if (verify)
            {
                var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
                var keyBytes = Encoding.UTF8.GetBytes(key);
                var algorithm = (string)headerData["alg"];

                var signature = HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign);
                var decodedCrypto = Convert.ToBase64String(crypto);
                var decodedSignature = Convert.ToBase64String(signature);

                if (decodedCrypto != decodedSignature)
                {
                    throw new ApplicationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
                }
            }

            return payloadData.ToString();
        }

        private static JwtHashAlgorithm GetHashAlgorithm(string algorithm)
        {
            switch (algorithm)
            {
                case "RS256": return JwtHashAlgorithm.RS256;
                case "HS384": return JwtHashAlgorithm.HS384;
                case "HS512": return JwtHashAlgorithm.HS512;
                default: throw new InvalidOperationException("Algorithm not supported.");
            }
        }

        // from JWT spec
        private static string Base64UrlEncode(byte[] input)
        {
            var output = Convert.ToBase64String(input);
            output = output.Split('=')[0]; // Remove any trailing '='s
            output = output.Replace('+', '-'); // 62nd char of encoding
            output = output.Replace('/', '_'); // 63rd char of encoding
            return output;
        }

        // from JWT spec
        private static byte[] Base64UrlDecode(string input)
        {
            var output = input;
            output = output.Replace('-', '+'); // 62nd char of encoding
            output = output.Replace('_', '/'); // 63rd char of encoding
            switch (output.Length % 4) // Pad with trailing '='s
            {
                case 0: break; // No pad chars in this case
                case 2: output += "=="; break; // Two pad chars
                case 3: output += "="; break; // One pad char
                default: throw new System.Exception("Illegal base64url string!");
            }
            var converted = Convert.FromBase64String(output); // Standard base64 decoder
            return converted;
        }
    }
}
Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
antoniuslin
  • 249
  • 2
  • 10
  • Can you include the expected output, or how you can tell if the generated JWT is correct? – Nate Barbettini Jul 06 '16 at 18:05
  • HI @NateBarbettini, thanks in advance. The output would be a "normal" JWT like so: "9s5LO7PAN2O8IKGse7rOTQKBqOHSTD1erPNNlrU8-Vpd1WLHq_09_yxfhal1kuZIlyaVjoLUzBM_xFi4nFlhfK_5v5YnSGpyPYCry24kJqAZPj3bjUE0ObMA-9gLxMIA_U5M5VAbIHmx1e1RdQ3MMH99MrybZJ-EzsLdQitcrJXvZy9qGNNkPAvDfBSO86kdeqv9z4g-mCpus7qAtlIqSilNjhuZilKqDuf2NZZSRrEfNZjhGlqEI6JW2G-MuVH9Q84PtwfcfqJHXjZo9bdLXDtxJB_dcfUSbblHYGfaiwbHCpHE_-YSXhuGo_TgXL4F_upux0xI3DG033UodC_1ea1XJuc" (Sorry I don't know how to separate these to its own line). And I used https://jwt.io/ to unpack it (with invalid signature error), and Firebase says invalid-unusable. – antoniuslin Jul 06 '16 at 18:09
  • 1
    For one the generator code for `RS256` is using Hmac. recheck the source – Nkosi Jul 06 '16 at 18:13
  • PS, that token in above comment is a "good" token from Firebase NodeJs library. My bad token usually looks similar to that except a bit shorter. – antoniuslin Jul 06 '16 at 18:14
  • I'm afraid it's a little beyond me. What should it be instead of hmac? – antoniuslin Jul 06 '16 at 18:20
  • @alien052002: I'm confused - that's not a valid JWT (as far as I can tell). – Nate Barbettini Jul 06 '16 at 18:33
  • 1
    Sorry! You're right. I pasted the wrong token from my copy&paste clipboard. Here's a correct one for sure (i 2x checked from jwt.io) (i may have to split as separate comments (too long). eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOiI1MjE0IiwiaWF0IjoxNDY3ODM2Mzg4LCJleHAiOjE0Njc4Mzk5ODgsImF1ZCI6Imh0dHBzOi8vaWRlbnRpdHl0b29sa2l0Lmdvb2dsZWFwaXMuY29tL2dvb2dsZS5pZGVudGl0eS5pZGVudGl0eXRvb2xraXQudjEuSWRlbnRpdHlUb29sa2l0IiwiaXNzIjoiZmlyZWJhc2UtYXV0aC10b2tlbkBzYXllci1jaGF0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwic3ViIjoiZmlyZWJhc2UtYXV0aC10b2tlbkBzYXllci1jaGF0Lmlh – antoniuslin Jul 06 '16 at 20:20
  • bS5nc2VydmljZWFjY291bnQuY29tIiwic3ViIjoiZmlyZWJhc2UtYXV0aC10b2tlbkBzYXllci1jaGF0LmlhbS5nc2VydmljZWFjY291bnQuY29tIn0.hWH5LGs2Q5vVUkdGIDy15kwP_8UB1S3yD3bQ12Mde8kqYyv2tUqAsmUE7OhTBqzAx6FCfNI3EoP182NOZUCRaGEBtF7_L0o9hFHFKruY5ZrmqnplaENQsntImhY4UZ-GdOUaPdAP-8wxcxY8m4RlghhF77c2v8F1Dox7XUEo8Xa-FTrK7JlCetlMP6bSlJ_Hdq5PnqKfdq4StjWT-eG0OGcl3oIkfgXsBGqZRDPGEbawV6TQ1FLPIOz4iKgnIWaOWWXEGiwUp1G3aYK1xYyJ0usQ4bzsittAp-a3xZkpgVok_-L2Y_gKuumbDIXiNgYiyxrDN5PF3rG0jAhu0r8BkA – antoniuslin Jul 06 '16 at 20:20
  • @alien052002, You should probably put the token in the question. When I copy from comments into jwt.io it is only making out the header – Nkosi Jul 06 '16 at 20:34
  • I was able to decode it in https://www.jsonwebtoken.io/ but I agree, it should be edited into the question itself. – Nate Barbettini Jul 06 '16 at 20:50

1 Answers1

2

The reason why the token is being rejected is because it's signed using the wrong algorithm: you're using HMAC-SHA256 (a symmetric key encryption algorithm) while the correct token uses RSA-SHA256 (an asymmetric or public/private key algorithm). You can see this in the header of your example token: "alg": "RS256"

I'd suggest using the classes in System.IdentityModel.Tokens.Jwt to simplify your code a lot:

public class GoogleJsonWebToken
{
    public static string Encode(string uid)
    {
        var firebaseInfPath = HttpContext.Current.Server.MapPath(WebConfigurationManager.AppSettings["firebaseInf"]);
        var firebaseInfJsonContent = File.ReadAllText(firebaseInfPath);
        var firebaseInf = JsonConvert.DeserializeObject<dynamic>(firebaseInfJsonContent);

        // NOTE: Replace this with your actual RSA public/private keypair!
        var provider = new RSACryptoServiceProvider(2048);
        var parameters = provider.ExportParameters(true);

        // Build the credentials used to sign the JWT
        var signingKey = new RsaSecurityKey(parameters);
        var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256);

        // Create a collection of optional claims
        var now = DateTimeOffset.UtcNow;
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, firebaseInf.client_email),
            new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
            new Claim("uid", uid, ClaimValueTypes.String), 
            new Claim("premium_account", "true", ClaimValueTypes.Boolean)
        };

        // Create and sign the JWT, and write it to a string
        var jwt = new JwtSecurityToken(
            issuer: firebaseInf.client_email,
            audience: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
            claims: claims,
            expires: now.AddMinutes(60).DateTime,
            signingCredentials: signingCredentials);
        return new JwtSecurityTokenHandler().WriteToken(jwt);
    }
}

Using a dummy key, this code creates a token that matches the header and payload schema of your example token:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmaXJlYmFzZS1hdXRoLXRva2VuQHNheWVyLWNoYXQuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJpYXQiOjE0Njc4MzgwODUsInVpZCI6IjUyMTQiLCJwcmVtaXVtX2FjY291bnQiOnRydWUsImV4cCI6MTQ2Nzg2Njg4NSwiaXNzIjoiZmlyZWJhc2UtYXV0aC10b2tlbkBzYXllci1jaGF0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwiYXVkIjoiaHR0cHM6Ly9pZGVudGl0eXRvb2xraXQuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmlkZW50aXR5LmlkZW50aXR5dG9vbGtpdC52MS5JZGVudGl0eVRvb2xraXQifQ.yMSGRpm4r3mPqiA9KnKVILVb8jT2Vbqcy4gvoLIugXnzLlw45F-GepAaBJK-j0-EN34WLsiLiLCRDJwW15TMasbrB1ZX3H4zd1by2GjZ1VmcUL8LITglICvs8CXkamjPjHeQUc4q--jhveKmTby8WHsH4b-HeiYoVl8JhxrAF13buNbiTq66dfvkl9q6mnyuKS_oyFB6_9WNphzHRa2BdJ51olq4qQsUNZ-nzOe4moHHjxpEzQfQpIe-QMJHdqojp9ukOW5eTMhFkQRPs3Bme4jpxIHPknC9j8YRvx_i0FbEJ8qAY2ujWcq80aC6YBKox55iP-AwfX_mEV7Tz14PBQ

You should be able to drop in your public/private key pair and use the above code to generate valid JWTs.

Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
  • @alien052002 Happy to help! Let me know if it works for you. – Nate Barbettini Jul 07 '16 at 01:33
  • There's a disconnect between "var parameters..." and the instantiation of RsaSecurityKey(parameters) since that class takes RSA and not RSA parameters. RSA itself seems to be an abstract class. What am I missing? – antoniuslin Jul 07 '16 at 03:31
  • I think I _might_ know what I don't understand. What you noted as: "// NOTE: Replace this with your actual RSA public/private keypair!" And I have a string private key like so: "var prikey = (string)firebaseInf.private_key; var pubkey = (string)firebaseInf.pub_key;" how will these get used to instantiate the RsaSecurityKey class whose constructor takes in an RSA object? I can cast the provider instead of the params, but I still don't know how to input our own private & public key in the provider object? I will try this: http://stackoverflow.com/a/13654510. Am I taking the right step? – antoniuslin Jul 07 '16 at 04:44
  • @alien052002 That's a good question and probably worthy of a separate Stack post. I don't know how to initialize the RSACryptoServiceProvider with strings off the top of my head. – Nate Barbettini Jul 07 '16 at 04:47
  • 1
    Np... good reading / research time for me too. :) And have received lots of great help from you already to jumpstart it. I will edit my question with the final solution if/when I find it. Thanks much again @NateBarbettini. – antoniuslin Jul 07 '16 at 05:00
  • In the firebase console I have a service account with a private key associated. But how I can get the public key? – JCarlosR Nov 15 '16 at 23:41
  • 1
    @JCarlos Go to your project in the Firebase Console. In the top left, you'll see a gear icon. Click it and go to "Project Settings". On the "General" tab, there will be a "Web API Key" field. That's your public key. – Jim Elrod Nov 16 '16 at 01:54
  • Hi @NateBarbettini I have tried your code. But I am not getting how to replace private keys with my. Can you please show me how can I replace it? – Ajay Jul 13 '17 at 12:47
  • @AjayPunekar Are you trying to generate a JWT signed with RSA, or validate one? You should post a new question and I'd be happy to take a look (link to it from here). – Nate Barbettini Jul 13 '17 at 17:40
  • @NateBarbettini Thanks for reply. Here is a link of my question https://stackoverflow.com/questions/45095126/c-sharp-generate-firebase-custom-token – Ajay Jul 14 '17 at 06:16
  • @NateBarbettini Hi Nate, Did you get a change to look at my question? I am really stuck on this issue. – Ajay Jul 15 '17 at 14:06
  • @NateBarbettini, where should private/public key be replaced? would you please add code for that with sample "xxxxx" keys – Reza Jul 29 '17 at 23:55
  • Where are you guys getting the firebase inf file from – Warren Dec 04 '17 at 22:13