saltyJeff
saltyJeff

Reputation: 67

C# Firebase JWT Token Verification

I'm trying to verify a token sent from the client side Firebase API on a C# Server. I've tried almost everything but for some reason I can't get the token verification method correct. I have implemented everything up to getting the public keys from the network, but I can't seem to figure out to get the RS256 algorithim to equal the JWT's 3rd chunk.

class FirebaseJWTAuth {
    public string FirebaseId;
    private HttpClient Req;

    //initialize all the settings
    public FirebaseJWTAuth(string firebaseId) {
        firebaseId = FirebaseId;
        Req = new HttpClient();
        Req.BaseAddress = new Uri("https://www.googleapis.com/robot/v1/metadata/");
    }

    //given a token, return the user id as a string if valid, null if invalid
    public async Task<string> Verify(string token) {
        //following instructions from https://firebase.google.com/docs/auth/admin/verify-id-tokens

        string hashChunk = token; //keep for hashing later on
        hashChunk = hashChunk.Substring(0, hashChunk.LastIndexOf('.'));

        token = token.Replace('-', '+').Replace('_', '/'); //sanitize for base64 on C#

        string[] sections = token.Split('.'); //split into 3 sections according to JWT standards
        JwtHeader header = B64Json<JwtHeader>(sections[0]);

        //verify the header
        if(header.alg != "RS256") {
            return null;
        }

        //get the public keys
        HttpResponseMessage res = await Req.GetAsync("x509/[email protected]"); //make async
        string keyDictStr = await res.Content.ReadAsStringAsync();
        Dictionary<string, string> keyDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(keyDictStr);
        string keyStr = null;
        keyDict.TryGetValue(header.kid, out keyStr);
        if(keyStr == null) {
            return null;
        }

        //Use the RSACryptoServiceProvider to verify the hash
        var rsaCrypto = CertFromPem(keyStr);
        byte[] plainText = Encoding.UTF8.GetBytes(hashChunk);
        byte[] hashed = SHA256Managed.Create().ComputeHash(plainText);
        byte[] encrypted = rsaCrypto.Encrypt(hashed, false);

        byte[] challenge = SafeB64Decode(sections[2]);

        Console.WriteLine(encrypted.SequenceEqual(challenge)); //QUESTION IN THE ISSUE: expect to be true, but always false

        //for debugging purposes
        Console.WriteLine(Convert.ToBase64String(challenge));
        Console.WriteLine(Convert.ToBase64String(encrypted));

        return "didn't really get down to this part";
    }

    //given a string, return the RSACryptoServiceProvider which corresponds to the public key
    static RSACryptoServiceProvider CertFromPem(string pemKey) {
        X509Certificate2 cert = new X509Certificate2();
        cert.Import(Encoding.UTF8.GetBytes(pemKey));
        Console.WriteLine(cert.ToString());
        return (RSACryptoServiceProvider) cert.PublicKey.Key;
    }

    //b64 decoding with padding to calm the C# converter
    static byte[] SafeB64Decode(string encoded) {
        string encodedPad = encoded + new string('=', encoded.Length % 4);
        return Convert.FromBase64String(encodedPad);
    }
    static string SafeB64DecodeStr(string encoded) {
        return Encoding.UTF8.GetString(SafeB64Decode(encoded));
    }
    static T B64Json<T> (string encoded) {
        string decoded = SafeB64DecodeStr(encoded);
        Console.WriteLine(decoded);
        return JsonConvert.DeserializeObject<T>(decoded);
    }

    //structs representing the first 2 chunks of a JWT
    private struct JwtHeader {
        public string alg;
        public string kid;
    }
    private struct JwtPayload {
        public long exp;
        public long iat;
        public string aud;
        public string iss;
        public string sub;
    }
}

I know this is rough (there's no verification of the payload) but I can't match the tokens. I'm just a junior developer but I've tried to add extra comments and spacing to make your reading easier.

I've been testing this code on my personal token which I am not uploading here for obvious reasons. However, if you want to test your code go on to the firebase-auth demo site and login. Then, open up a DevTools console and type firebase.auth().currentUser.getIdToken(true).then(console.log). Your token will pop up in the console.

Thanks in advance.

Upvotes: 0

Views: 1023

Answers (1)

saltyJeff
saltyJeff

Reputation: 67

/facepalm

I forgot that RSA Signing != RSA Encrypting and apparently C# has different libraries for both. The magic class was RSAPKCS1SignatureDeformatter.

The answer is to use the RSAPKCS1SignatureDeformatter.VerifySignature method instead of the encryption-based RSACryptoServiceProvider

Upvotes: 2

Related Questions