using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
namespace Jwt
{
///
/// Encoding and decoding for JSON Web Tokens.
///
public static class JsonWebToken
{
///
/// Gets or sets the implementation being used.
///
public static IJsonSerializer JsonSerializer = new DefaultJsonSerializer();
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
///
/// Creates a JWT using the specified payload and key hashed with .
///
/// An arbitrary payload (must be serializable to JSON).
/// The key bytes used to sign the token.
/// The generated JWT.
public static string Encode(object payload, byte[] key)
=> Encode(new JwtData { Payload = payload, KeyBytes = key, Algorithm = JwtHashAlgorithm.HS256 });
///
/// Creates a JWT using the specified payload, key and algorithm.
///
/// An arbitrary payload (must be serializable to JSON).
/// The key bytes used to sign the token.
/// The hash algorithm to use.
/// The generated JWT.
public static string Encode(object payload, byte[] key, JwtHashAlgorithm algorithm)
=> Encode(new JwtData { Payload = payload, KeyBytes = key, Algorithm = algorithm });
///
/// Creates a JWT using the specified payload and key hashed with .
///
/// An arbitrary payload (must be serializable to JSON).
/// The key used to sign the token.
/// The generated JWT.
public static string Encode(object payload, string key)
=> Encode(new JwtData { Payload = payload, Key = key, Algorithm = JwtHashAlgorithm.HS256 });
///
/// Creates a JWT using the specified payload, key and algorithm.
///
/// An arbitrary payload (must be serializable to JSON).
/// The key used to sign the token.
/// The hash algorithm to use.
/// The generated JWT.
public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
=> Encode(new JwtData { Payload = payload, Key = key, Algorithm = algorithm });
///
/// Creates a JWT using the specified .
///
/// A object.
/// The generated JWT.
public static string Encode(JwtData data)
{
var header = new Dictionary(data.ExtraHeaders ?? new Dictionary())
{
{ "typ", "JWT" },
{ "alg", data.Algorithm.ToString() }
};
var headerBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(header));
var payloadBytes = Encoding.UTF8.GetBytes(data.Serialized ? (string)data.Payload : JsonSerializer.Serialize(data.Payload));
var segments = new List
{
Base64UrlEncode(headerBytes),
Base64UrlEncode(payloadBytes)
};
var bytesToSign = Encoding.UTF8.GetBytes(string.Join(".", segments));
var keyBytes = data.KeyBytes;
if (keyBytes == null || keyBytes.Length == 0)
{
keyBytes = Encoding.UTF8.GetBytes(data.Key);
}
var signature = ComputeHash(data.Algorithm, keyBytes, bytesToSign);
segments.Add(Base64UrlEncode(signature));
return string.Join(".", segments);
}
///
/// Decodes the specified JWT and returns the JSON payload.
///
/// The JWT.
/// The key that was used to sign the JWT.
/// Whether to verify the signature (default is true).
/// A string containing the JSON payload.
///
/// If the parameter was true and the signature was not valid
/// or if the JWT was signed with an unsupported algorithm.
///
///
/// When the given doesn't consist of 3 parts delimited by dots.
///
public static string Decode(string token, string key, bool verify = true)
=> Decode(token, Encoding.UTF8.GetBytes(key), verify);
///
/// Decodes the JWT token and deserializes JSON payload to a dictionary.
///
/// The JWT.
/// The key that was used to sign the JWT.
/// Whether to verify the signature (default is true).
/// An object representing the payload.
///
/// If the parameter was true and the signature was not valid
/// or if the JWT was signed with an unsupported algorithm.
///
///
/// When the given doesn't consist of 3 parts delimited by dots.
///
public static Dictionary DecodeToObject(string token, string key, bool verify = true)
=> DecodeToObject>(token, key, verify);
///
/// Decodes the JWT token and deserializes JSON payload to a dictionary.
///
/// The JWT.
/// The key that was used to sign the JWT.
/// Whether to verify the signature (default is true).
/// An object representing the payload.
///
/// If the parameter was true and the signature was not valid
/// or if the JWT was signed with an unsupported algorithm.
///
///
/// When the given doesn't consist of 3 parts delimited by dots.
///
public static Dictionary DecodeToObject(string token, byte[] key, bool verify = true)
=> DecodeToObject>(token, key, verify);
///
/// Decodes the JWT token and deserializes JSON payload to .
///
/// The type of the object.
/// The JWT.
/// The key that was used to sign the JWT.
/// Whether to verify the signature (default is true).
/// An object representing the payload.
///
/// If the parameter was true and the signature was not valid
/// or if the JWT was signed with an unsupported algorithm.
///
///
/// When the given doesn't consist of 3 parts delimited by dots.
///
public static T DecodeToObject(string token, string key, bool verify = true)
=> DecodeToObject(token, Encoding.UTF8.GetBytes(key), verify);
///
/// Decodes the JWT token and deserializes JSON payload to .
///
/// The to return
/// The JWT.
/// The key that was used to sign the JWT.
/// Whether to verify the signature (default is true).
/// An object representing the payload.
///
/// If the parameter was true and the signature was not valid
/// or if the JWT was signed with an unsupported algorithm.
///
///
/// When the given doesn't consist of 3 parts delimited by dots.
///
public static T DecodeToObject(string token, byte[] key, bool verify = true)
=> JsonSerializer.Deserialize(Decode(token, key, verify));
///
/// Decodes the specified JWT and returns the JSON payload.
///
/// The JWT.
/// The key bytes that were used to sign the JWT.
/// Whether to verify the signature (default is true).
/// A string containing the JSON payload.
///
/// If the parameter was true and the signature was not valid
/// or if the JWT was signed with an unsupported algorithm.
///
///
/// When the given doesn't consist of 3 parts delimited by dots.
///
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}'.", 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>(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 jwtSignature, string computedSignature, string payloadJson)
{
// Compare the signature from the JWT and the computed signature.
if (jwtSignature != computedSignature)
{
throw new SignatureVerificationException("Invalid JWT signature.");
}
// 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.TryGetValue("exp", out var expObj) && expObj != null)
{
// Safely unpack a boxed int.
int exp;
try
{
exp = Convert.ToInt32(expObj);
}
catch (Exception)
{
throw new SignatureVerificationException($"Claim 'exp' must be an integer. Given claim: '{expObj}'.");
}
var secondsSinceEpoch = Math.Round((DateTime.UtcNow - UnixEpoch).TotalSeconds);
if (secondsSinceEpoch >= exp)
{
throw new SignatureVerificationException("Token has expired.");
}
}
}
private static byte[] ComputeHash(JwtHashAlgorithm algorithm, byte[] key, byte[] value)
{
HashAlgorithm hashAlgorithm;
switch (algorithm)
{
case JwtHashAlgorithm.HS256:
hashAlgorithm = new HMACSHA256(key);
break;
case JwtHashAlgorithm.HS384:
hashAlgorithm = new HMACSHA384(key);
break;
case JwtHashAlgorithm.HS512:
hashAlgorithm = new HMACSHA512(key);
break;
default:
throw new Exception($"Unsupported hash algorithm: '{algorithm}'.");
}
using (hashAlgorithm)
{
return hashAlgorithm.ComputeHash(value);
}
}
private static JwtHashAlgorithm GetHashAlgorithm(string algorithm)
{
switch (algorithm)
{
case "HS256":
return JwtHashAlgorithm.HS256;
case "HS384":
return JwtHashAlgorithm.HS384;
case "HS512":
return JwtHashAlgorithm.HS512;
default:
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.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
// Pad with trailing '='s
switch (output.Length % 4)
{
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 Exception($"Illegal base-64 string: '{input}'.");
}
return output;
}
}
}