Skip to content

Commit

Permalink
Several improvements
Browse files Browse the repository at this point in the history
- Added overload with default HS256 algorithm
- Breaking: Return Dictionary<string, object> from DecodeToObject,
  rather than wrapping it in an object
- Only decode header and signature when verifying
- Remove some unnecessary base-64 decoding when verifying
  • Loading branch information
henkmollema committed Feb 10, 2018
1 parent 905d84c commit a383796
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 94 deletions.
180 changes: 96 additions & 84 deletions src/JsonWebTokens/JsonWebToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace Jwt
{
/// <summary>
/// Provides methods for encoding and decoding JSON Web Tokens.
/// Encoding and decoding for JSON Web Tokens.
/// </summary>
public static class JsonWebToken
{
Expand All @@ -17,6 +17,15 @@ public static class JsonWebToken

private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);

/// <summary>
/// Creates a JWT using the specified payload and key hashed with <see cref="JwtHashAlgorithm.HS256 "/>.
/// </summary>
/// <param name="payload">An arbitrary payload (must be serializable to JSON).</param>
/// <param name="key">The key bytes used to sign the token.</param>
/// <returns>The generated JWT.</returns>
public static string Encode(object payload, byte[] key)
=> Encode(new JwtData { Payload = payload, KeyBytes = key, Algorithm = JwtHashAlgorithm.HS256 });

/// <summary>
/// Creates a JWT using the specified payload, key and algorithm.
/// </summary>
Expand All @@ -25,9 +34,16 @@ public static class JsonWebToken
/// <param name="algorithm">The hash algorithm to use.</param>
/// <returns>The generated JWT.</returns>
public static string Encode(object payload, byte[] key, JwtHashAlgorithm algorithm)
{
return Encode(new JwtData { Payload = payload, KeyBytes = key, Algorithm = algorithm });
}
=> Encode(new JwtData { Payload = payload, KeyBytes = key, Algorithm = algorithm });

/// <summary>
/// Creates a JWT using the specified payload and key hashed with <see cref="JwtHashAlgorithm.HS256 "/>.
/// </summary>
/// <param name="payload">An arbitrary payload (must be serializable to JSON).</param>
/// <param name="key">The key used to sign the token.</param>
/// <returns>The generated JWT.</returns>
public static string Encode(object payload, string key)
=> Encode(new JwtData { Payload = payload, Key = key, Algorithm = JwtHashAlgorithm.HS256 });

/// <summary>
/// Creates a JWT using the specified payload, key and algorithm.
Expand All @@ -37,9 +53,7 @@ public static string Encode(object payload, byte[] key, JwtHashAlgorithm algorit
/// <param name="algorithm">The hash algorithm to use.</param>
/// <returns>The generated JWT.</returns>
public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
{
return Encode(new JwtData { Payload = payload, Key = key, Algorithm = algorithm });
}
=> Encode(new JwtData { Payload = payload, Key = key, Algorithm = algorithm });

/// <summary>
/// Creates a JWT using the specified <see cref="JwtData"/>.
Expand Down Expand Up @@ -92,61 +106,27 @@ public static string Encode(JwtData data)
/// When the given <paramref name="token"/> doesn't consist of 3 parts delimited by dots.
/// </exception>
public static string Decode(string token, string key, bool verify = true)
{
return Decode(token, Encoding.UTF8.GetBytes(key), verify);
}
=> Decode(token, Encoding.UTF8.GetBytes(key), verify);

/// <summary>
/// Decodes the specified JWT and returns the JSON payload.
/// Decodes the JWT token and deserializes JSON payload to a dictionary.
/// </summary>
/// <param name="token">The JWT.</param>
/// <param name="key">The key bytes that were used to sign the JWT.</param>
/// <param name="key">The key that was used to sign the JWT.</param>
/// <param name="verify">Whether to verify the signature (default is true).</param>
/// <returns>A string containing the JSON payload.</returns>
/// <returns>An object representing the payload.</returns>
/// <exception cref="SignatureVerificationException">
/// If the <paramref name="verify"/> parameter was true and the signature was not valid
/// or if the JWT was signed with an unsupported algorithm.
/// </exception>
/// <exception cref="ArgumentException">
/// When the given <paramref name="token"/> doesn't consist of 3 parts delimited by dots.
/// </exception>
public static string Decode(string token, byte[] key, bool verify = true)
{
var parts = token.Split('.');
if (parts.Length != 3)
{
throw new ArgumentException($"Token must consist of 3 parts delimited by dot. Given token: '{token}'.");
}

var header = parts[0];
var payload = parts[1];
var crypto = Base64UrlDecode(parts[2]);

var headerBytes = Base64UrlDecode(header);
var headerJson = Encoding.UTF8.GetString(headerBytes, 0, headerBytes.Length);

var payloadBytes = Base64UrlDecode(payload);
var payloadJson = Encoding.UTF8.GetString(payloadBytes, 0, payloadBytes.Length);

var headerData = JsonSerializer.Deserialize<IDictionary<string, object>>(headerJson);

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

var signature = ComputeHash(GetHashAlgorithm(algorithm), key, bytesToSign);
var decodedCrypto = Convert.ToBase64String(crypto);
var decodedSignature = Convert.ToBase64String(signature);

Verify(decodedCrypto, decodedSignature, payloadJson);
}

return payloadJson;
}
public static Dictionary<string, object> DecodeToObject(string token, string key, bool verify = true)
=> DecodeToObject<Dictionary<string, object>>(token, key, verify);

/// <summary>
/// Decodes the JWT token and deserializes JSON payload to an <see cref="object"/>.
/// Decodes the JWT token and deserializes JSON payload to a dictionary.
/// </summary>
/// <param name="token">The JWT.</param>
/// <param name="key">The key that was used to sign the JWT.</param>
Expand All @@ -159,14 +139,13 @@ public static string Decode(string token, byte[] key, bool verify = true)
/// <exception cref="ArgumentException">
/// When the given <paramref name="token"/> doesn't consist of 3 parts delimited by dots.
/// </exception>
public static object DecodeToObject(string token, string key, bool verify = true)
{
return DecodeToObject(token, Encoding.UTF8.GetBytes(key), verify);
}
public static Dictionary<string, object> DecodeToObject(string token, byte[] key, bool verify = true)
=> DecodeToObject<Dictionary<string, object>>(token, key, verify);

/// <summary>
/// Decodes the JWT token and deserializes JSON payload to an <see cref="object"/>.
/// Decodes the JWT token and deserializes JSON payload to <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the object.</typeparam>
/// <param name="token">The JWT.</param>
/// <param name="key">The key that was used to sign the JWT.</param>
/// <param name="verify">Whether to verify the signature (default is true).</param>
Expand All @@ -178,17 +157,13 @@ public static object DecodeToObject(string token, string key, bool verify = true
/// <exception cref="ArgumentException">
/// When the given <paramref name="token"/> doesn't consist of 3 parts delimited by dots.
/// </exception>
public static object DecodeToObject(string token, byte[] key, bool verify = true)
{
var payloadJson = Decode(token, key, verify);
var payloadData = JsonSerializer.Deserialize<Dictionary<string, object>>(payloadJson);
return payloadData;
}
public static T DecodeToObject<T>(string token, string key, bool verify = true)
=> DecodeToObject<T>(token, Encoding.UTF8.GetBytes(key), verify);

/// <summary>
/// Decodes the JWT token and deserializes JSON payload to <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the object.</typeparam>
/// <typeparam name="T">The <see cref="Type"/> to return</typeparam>
/// <param name="token">The JWT.</param>
/// <param name="key">The key that was used to sign the JWT.</param>
/// <param name="verify">Whether to verify the signature (default is true).</param>
Expand All @@ -200,53 +175,80 @@ public static object DecodeToObject(string token, byte[] key, bool verify = true
/// <exception cref="ArgumentException">
/// When the given <paramref name="token"/> doesn't consist of 3 parts delimited by dots.
/// </exception>
public static T DecodeToObject<T>(string token, string key, bool verify = true)
{
return DecodeToObject<T>(token, Encoding.UTF8.GetBytes(key), verify);
}
public static T DecodeToObject<T>(string token, byte[] key, bool verify = true)
=> JsonSerializer.Deserialize<T>(Decode(token, key, verify));

/// <summary>
/// Decodes the JWT token and deserializes JSON payload to <typeparamref name="T"/>.
/// Decodes the specified JWT and returns the JSON payload.
/// </summary>
/// <typeparam name="T">The <see cref="Type"/> to return</typeparam>
/// <param name="token">The JWT.</param>
/// <param name="key">The key that was used to sign the JWT.</param>
/// <param name="key">The key bytes that were used to sign the JWT.</param>
/// <param name="verify">Whether to verify the signature (default is true).</param>
/// <returns>An object representing the payload.</returns>
/// <returns>A string containing the JSON payload.</returns>
/// <exception cref="SignatureVerificationException">
/// If the <paramref name="verify"/> parameter was true and the signature was not valid
/// or if the JWT was signed with an unsupported algorithm.
/// </exception>
/// <exception cref="ArgumentException">
/// When the given <paramref name="token"/> doesn't consist of 3 parts delimited by dots.
/// </exception>
public static T DecodeToObject<T>(string token, byte[] key, bool verify = true)
public static string Decode(string token, byte[] key, bool verify = true)
{
var payloadJson = Decode(token, key, verify);
var payloadData = JsonSerializer.Deserialize<T>(payloadJson);
return payloadData;
var parts = token.Split('.');
if (parts.Length != 3)
{
throw new ArgumentException($"Token must consist of 3 parts delimited by dot. Given token: '{token}'.", nameof(token));
}

// Decode JWT payload
var payload = parts[1];
var payloadBytes = Base64UrlDecode(payload);
var payloadJson = Encoding.UTF8.GetString(payloadBytes);

if (verify)
{
// Decode JWT header.
var header = parts[0];
var headerBytes = Base64UrlDecode(header);
var headerJson = Encoding.UTF8.GetString(headerBytes);

// Decode the signature from the JWT.
var jwtSignature = UrlDecode(parts[2]);

// Compute the signature for the JWT.
var headerData = JsonSerializer.Deserialize<IDictionary<string, object>>(headerJson);
var algorithm = (string)headerData["alg"];
var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
var signature = ComputeHash(GetHashAlgorithm(algorithm), key, bytesToSign);
var computedSignature = Convert.ToBase64String(signature);

Verify(jwtSignature, computedSignature, payloadJson);
}

return payloadJson;
}

private static void Verify(string decodedCrypto, string decodedSignature, string payloadJson)
private static void Verify(string jwtSignature, string computedSignature, string payloadJson)
{
if (decodedCrypto != decodedSignature)
// Compare the signature from the JWT and the computed signature.
if (jwtSignature != computedSignature)
{
throw new SignatureVerificationException($"Invalid signature. Expected '{decodedCrypto}' got '{decodedSignature}'.");
throw new SignatureVerificationException($"Invalid signature. Expected '{jwtSignature}' got '{computedSignature}'.");
}

// Verify exp claim: https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.4
var payloadData = JsonSerializer.Deserialize<IDictionary<string, object>>(payloadJson);
if (payloadData.ContainsKey("exp") && payloadData["exp"] != null)
if (payloadData.TryGetValue("exp", out var expObj) && expObj != null)
{
// Safely unpack a boxed int.
int exp;
try
{
exp = Convert.ToInt32(payloadData["exp"]);
exp = Convert.ToInt32(expObj);
}
catch (Exception)
{
throw new SignatureVerificationException($"Claim 'exp' must be an integer. Given claim: '{payloadData["exp"]}'.");
throw new SignatureVerificationException($"Claim 'exp' must be an integer. Given claim: '{expObj}'.");
}

var secondsSinceEpoch = Math.Round((DateTime.UtcNow - UnixEpoch).TotalSeconds);
Expand Down Expand Up @@ -292,25 +294,36 @@ private static JwtHashAlgorithm GetHashAlgorithm(string algorithm)
case "HS512":
return JwtHashAlgorithm.HS512;
default:
throw new SignatureVerificationException("Algorithm not supported.");
throw new SignatureVerificationException($"Algorithm '{algorithm}' not supported.");
}
}

private static readonly char[] _padding = new [] { '=' };

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

private static byte[] Base64UrlDecode(string input)
{
var output = UrlDecode(input);
var converted = Convert.FromBase64String(output);
return converted;
}

private static string UrlDecode(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

// Pad with trailing '='s
switch (output.Length % 4)
{
case 0:
break; // No pad chars in this case
Expand All @@ -321,11 +334,10 @@ private static byte[] Base64UrlDecode(string input)
output += "=";
break; // One pad char
default:
throw new Exception($"Illegal base64url string: '{input}'.");
throw new Exception($"Illegal base-64 string: '{input}'.");
}

var converted = Convert.FromBase64String(output);
return converted;
return output;
}
}
}
10 changes: 5 additions & 5 deletions src/JsonWebTokens/JsonWebTokens.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>JSON Web Tokens implementation for .NET Core.</Description>
<Copyright>Copyright 2017 Henk Mollema</Copyright>
<VersionPrefix>2.0.0</VersionPrefix>
<Description>JSON Web Tokens implementation for .NET Standard.</Description>
<Copyright>Copyright 2018 Henk Mollema</Copyright>
<VersionPrefix>3.0.0</VersionPrefix>
<Authors>Henk Mollema</Authors>
<TargetFrameworks>netstandard1.3</TargetFrameworks>
<TargetFrameworks>netstandard1.3;netstandard2.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>JsonWebTokens</AssemblyName>
<PackageId>JsonWebTokens</PackageId>
Expand All @@ -17,4 +17,4 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
</ItemGroup>
</Project>
</Project>
13 changes: 8 additions & 5 deletions src/JsonWebTokens/JwtBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,12 @@ public JwtBuilder WithHeaders(IDictionary<string, object> dict)
/// Builds the data to a <see cref="JwtData"/> object.
/// </summary>
/// <returns>A <see cref="JwtData"/> object.</returns>
public JwtData Build()
{
return _data;
}
public JwtData Build() => _data;

/// <summary>
/// Builds and encodes the current <see cref="JwtBuilder"/> object.
/// </summary>
/// <returns>An encoded JSON Web Token.</returns>
public string Encode() => JsonWebToken.Encode(Build());
}
}
}

0 comments on commit a383796

Please sign in to comment.