diff --git a/JWT.Tests/DecodeTests.cs b/JWT.Tests/DecodeTests.cs index 295f8d2f3..f0d7ed1eb 100644 --- a/JWT.Tests/DecodeTests.cs +++ b/JWT.Tests/DecodeTests.cs @@ -7,14 +7,15 @@ namespace JWT.Tests { [TestClass] - public class DecodeTests { - JavaScriptSerializer jsonSerializer = new JavaScriptSerializer(); - string token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJGaXJzdE5hbWUiOiJCb2IiLCJBZ2UiOjM3fQ.cr0xw8c_HKzhFBMQrseSPGoJ0NPlRp_3BKzP96jwBdY"; - Customer customer = new Customer() { FirstName = "Bob", Age = 37 }; + private static readonly Customer customer = new Customer { FirstName = "Bob", Age = 37 }; - private Dictionary dictionaryPayload = new Dictionary() { + private const string token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJGaXJzdE5hbWUiOiJCb2IiLCJBZ2UiOjM3fQ.cr0xw8c_HKzhFBMQrseSPGoJ0NPlRp_3BKzP96jwBdY"; + private const string malformedtoken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9eyJGaXJzdE5hbWUiOiJCb2IiLCJBZ2UiOjM3fQ.cr0xw8c_HKzhFBMQrseSPGoJ0NPlRp_3BKzP96jwBdY"; + + private static readonly IDictionary dictionaryPayload = new Dictionary + { { "FirstName", "Bob" }, { "Age", 37 } }; @@ -22,6 +23,7 @@ public class DecodeTests [TestMethod] public void Should_Decode_Token_To_Json_Encoded_String() { + var jsonSerializer = new JavaScriptSerializer(); var expectedPayload = jsonSerializer.Serialize(customer); string decodedPayload = JsonWebToken.Decode(token, "ABC", false); @@ -34,7 +36,7 @@ public void Should_Decode_Token_To_Dictionary() { object decodedPayload = JsonWebToken.DecodeToObject(token, "ABC", false); - decodedPayload.ShouldBeEquivalentTo(dictionaryPayload, options=>options.IncludingAllRuntimeProperties()); + decodedPayload.ShouldBeEquivalentTo(dictionaryPayload, options => options.IncludingAllRuntimeProperties()); } [TestMethod] @@ -48,14 +50,15 @@ public void Should_Decode_Token_To_Dictionary_With_ServiceStack() } [TestMethod] - public void Should_Decode_Token_To_Dictionary_With_Newtonsoft() { + public void Should_Decode_Token_To_Dictionary_With_Newtonsoft() + { JsonWebToken.JsonSerializer = new NewtonJsonSerializer(); object decodedPayload = JsonWebToken.DecodeToObject(token, "ABC", false); decodedPayload.ShouldBeEquivalentTo(dictionaryPayload, options => options.IncludingAllRuntimeProperties()); } - + [TestMethod] public void Should_Decode_Token_To_Generic_Type() { @@ -65,7 +68,8 @@ public void Should_Decode_Token_To_Generic_Type() } [TestMethod] - public void Should_Decode_Token_To_Generic_Type_With_ServiceStack() { + public void Should_Decode_Token_To_Generic_Type_With_ServiceStack() + { JsonWebToken.JsonSerializer = new ServiceStackJsonSerializer(); Customer decodedPayload = JsonWebToken.DecodeToObject(token, "ABC", false); @@ -74,7 +78,8 @@ public void Should_Decode_Token_To_Generic_Type_With_ServiceStack() { } [TestMethod] - public void Should_Decode_Token_To_Generic_Type_With_Newtonsoft() { + public void Should_Decode_Token_To_Generic_Type_With_Newtonsoft() + { JsonWebToken.JsonSerializer = new NewtonJsonSerializer(); Customer decodedPayload = JsonWebToken.DecodeToObject(token, "ABC", false); @@ -84,9 +89,8 @@ public void Should_Decode_Token_To_Generic_Type_With_Newtonsoft() { [TestMethod] [ExpectedException(typeof(ArgumentException))] - public void Should_Throw_On_Malformed_Token() { - string malformedtoken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9eyJGaXJzdE5hbWUiOiJCb2IiLCJBZ2UiOjM3fQ.cr0xw8c_HKzhFBMQrseSPGoJ0NPlRp_3BKzP96jwBdY"; - + public void Should_Throw_On_Malformed_Token() + { JsonWebToken.DecodeToObject(malformedtoken, "ABC", false); } @@ -115,14 +119,16 @@ public void Should_Throw_On_Expired_Token() var anHourAgoUtc = DateTime.UtcNow.Subtract(new TimeSpan(1, 0, 0)); Int32 unixTimestamp = (Int32)(anHourAgoUtc.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; - var invalidexptoken = JsonWebToken.Encode(new { exp=unixTimestamp }, "ABC", JwtHashAlgorithm.HS256); + var invalidexptoken = JsonWebToken.Encode(new { exp = unixTimestamp }, "ABC", JwtHashAlgorithm.HS256); JsonWebToken.DecodeToObject(invalidexptoken, "ABC", true); } } - public class Customer { - public string FirstName {get;set;} - public int Age {get;set;} + public class Customer + { + public string FirstName { get; set; } + + public int Age { get; set; } } -} +} \ No newline at end of file diff --git a/JWT.Tests/EncodeTests.cs b/JWT.Tests/EncodeTests.cs index 24924e0f3..2bdde5810 100644 --- a/JWT.Tests/EncodeTests.cs +++ b/JWT.Tests/EncodeTests.cs @@ -6,9 +6,10 @@ namespace JWT.Tests [TestClass] public class EncodeTests { - string token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJGaXJzdE5hbWUiOiJCb2IiLCJBZ2UiOjM3fQ.cr0xw8c_HKzhFBMQrseSPGoJ0NPlRp_3BKzP96jwBdY"; - string extraheaderstoken = "eyJmb28iOiJiYXIiLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJGaXJzdE5hbWUiOiJCb2IiLCJBZ2UiOjM3fQ.slrbXF9VSrlX7LKsV-Umb_zEzWLxQjCfUOjNTbvyr1g"; - Customer customer = new Customer() { FirstName = "Bob", Age = 37 }; + private readonly static Customer customer = new Customer { FirstName = "Bob", Age = 37 }; + + private const string token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJGaXJzdE5hbWUiOiJCb2IiLCJBZ2UiOjM3fQ.cr0xw8c_HKzhFBMQrseSPGoJ0NPlRp_3BKzP96jwBdY"; + private const string extraheaderstoken = "eyJmb28iOiJiYXIiLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJGaXJzdE5hbWUiOiJCb2IiLCJBZ2UiOjM3fQ.slrbXF9VSrlX7LKsV-Umb_zEzWLxQjCfUOjNTbvyr1g"; [TestMethod] public void Should_Encode_Type() @@ -21,8 +22,8 @@ public void Should_Encode_Type() [TestMethod] public void Should_Encode_Type_With_Extra_Headers() { - var extraheaders = new Dictionary() { {"foo", "bar"} }; - + var extraheaders = new Dictionary { { "foo", "bar" } }; + string result = JsonWebToken.Encode(extraheaders, customer, "ABC", JwtHashAlgorithm.HS256); Assert.AreEqual(extraheaderstoken, result); @@ -38,17 +39,19 @@ public void Should_Encode_Type_With_ServiceStack() } [TestMethod] - public void Should_Encode_Type_With_ServiceStack_And_Extra_Headers() { + public void Should_Encode_Type_With_ServiceStack_And_Extra_Headers() + { JsonWebToken.JsonSerializer = new ServiceStackJsonSerializer(); - - var extraheaders = new Dictionary() { { "foo", "bar" } }; + + var extraheaders = new Dictionary { { "foo", "bar" } }; string result = JsonWebToken.Encode(extraheaders, customer, "ABC", JwtHashAlgorithm.HS256); Assert.AreEqual(extraheaderstoken, result); } [TestMethod] - public void Should_Encode_Type_With_Newtonsoft_Serializer() { + public void Should_Encode_Type_With_Newtonsoft_Serializer() + { JsonWebToken.JsonSerializer = new NewtonJsonSerializer(); string result = JsonWebToken.Encode(customer, "ABC", JwtHashAlgorithm.HS256); @@ -56,13 +59,14 @@ public void Should_Encode_Type_With_Newtonsoft_Serializer() { } [TestMethod] - public void Should_Encode_Type_With_Newtonsoft_Serializer_And_Extra_Headers() { + public void Should_Encode_Type_With_Newtonsoft_Serializer_And_Extra_Headers() + { JsonWebToken.JsonSerializer = new NewtonJsonSerializer(); - var extraheaders = new Dictionary() { { "foo", "bar" } }; + var extraheaders = new Dictionary { { "foo", "bar" } }; string result = JsonWebToken.Encode(extraheaders, customer, "ABC", JwtHashAlgorithm.HS256); Assert.AreEqual(extraheaderstoken, result); } } -} +} \ No newline at end of file diff --git a/JWT/JWT.cs b/JWT/JWT.cs index 0ee2b1eab..6f762b406 100755 --- a/JWT/JWT.cs +++ b/JWT/JWT.cs @@ -17,13 +17,15 @@ public enum JwtHashAlgorithm /// public static class JsonWebToken { - private static Dictionary> HashAlgorithms; + private static readonly IDictionary> HashAlgorithms; /// /// Pluggable JSON Serializer /// public static IJsonSerializer JsonSerializer = new DefaultJsonSerializer(); + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + static JsonWebToken() { HashAlgorithms = new Dictionary> @@ -47,8 +49,8 @@ public static string Encode(IDictionary extraHeaders, object pay var segments = new List(); var header = new Dictionary(extraHeaders) { - {"typ", "JWT"}, - {"alg", algorithm.ToString()} + { "typ", "JWT" }, + { "alg", algorithm.ToString() } }; byte[] headerBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(header)); @@ -121,7 +123,7 @@ public static string Decode(string token, byte[] key, bool verify = true) } var header = parts[0]; var payload = parts[1]; - byte[] crypto = Base64UrlDecode(parts[2]); + var crypto = Base64UrlDecode(parts[2]); var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header)); var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload)); @@ -137,32 +139,40 @@ public static string Decode(string token, byte[] key, bool verify = true) var decodedCrypto = Convert.ToBase64String(crypto); var decodedSignature = Convert.ToBase64String(signature); - if (decodedCrypto != decodedSignature) + Verify(decodedCrypto, decodedSignature, payloadJson); + } + + return payloadJson; + } + + private static void Verify(string decodedCrypto, string decodedSignature, string payloadJson) + { + if (decodedCrypto != decodedSignature) + { + throw new SignatureVerificationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature)); + } + + // verify exp claim https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.4 + var payloadData = JsonSerializer.Deserialize>(payloadJson); + if (payloadData.ContainsKey("exp") && payloadData["exp"] != null) + { + // safely unpack a boxed int + int exp; + try + { + exp = Convert.ToInt32(payloadData["exp"]); + } + catch (Exception) { - throw new SignatureVerificationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature)); + throw new SignatureVerificationException("Claim 'exp' must be an integer."); } - // verify exp claim https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.4 - var payloadData = JsonSerializer.Deserialize>(payloadJson); - if (payloadData.ContainsKey("exp") && payloadData["exp"] != null) + var secondsSinceEpoch = Math.Round((DateTime.UtcNow - UnixEpoch).TotalSeconds); + if (secondsSinceEpoch >= exp) { - // safely unpack a boxed int - int exp; - try { exp = Convert.ToInt32(payloadData["exp"]); } - catch (Exception) - { - throw new SignatureVerificationException("Claim 'exp' must be an integer."); - } - - var secondsSinceEpoch = Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds); - if (secondsSinceEpoch >= exp) - { - throw new SignatureVerificationException("Token has expired."); - } + throw new SignatureVerificationException("Token has expired."); } } - - return payloadJson; } /// @@ -188,7 +198,7 @@ public static string Decode(string token, string key, bool verify = true) /// Thrown if the verify parameter was true and the signature was NOT valid or if the JWT was signed with an unsupported algorithm. public static object DecodeToObject(string token, byte[] key, bool verify = true) { - var payloadJson = JsonWebToken.Decode(token, key, verify); + var payloadJson = Decode(token, key, verify); var payloadData = JsonSerializer.Deserialize>(payloadJson); return payloadData; } @@ -217,7 +227,7 @@ public static object DecodeToObject(string token, string key, bool verify = true /// Thrown if the verify parameter was true and the signature was NOT valid or if the JWT was signed with an unsupported algorithm. public static T DecodeToObject(string token, byte[] key, bool verify = true) { - var payloadJson = JsonWebToken.Decode(token, key, verify); + var payloadJson = Decode(token, key, verify); var payloadData = JsonSerializer.Deserialize(payloadJson); return payloadData; } @@ -267,19 +277,11 @@ public static byte[] Base64UrlDecode(string input) { 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!"); + case 3: output += "="; break; // One pad char + default: throw new Exception("Illegal base64url string!"); } var converted = Convert.FromBase64String(output); // Standard base64 decoder return converted; } } - - public class SignatureVerificationException : Exception - { - public SignatureVerificationException(string message) - : base(message) - { - } - } } diff --git a/JWT/JWT.csproj b/JWT/JWT.csproj index 8ec8ebe14..ecdb738fb 100644 --- a/JWT/JWT.csproj +++ b/JWT/JWT.csproj @@ -42,6 +42,7 @@ +