using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using JWT;
namespace Hncore.Infrastructure.WebApi
{
public class ValidatorOption
{
public bool ValidateLifetime = true;
}
public sealed class EtorJwtValidator : IJwtValidator
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly ValidatorOption _option;
///
/// Creates an instance of
///
/// The Json Serializer
/// The DateTime Provider
public EtorJwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimeProvider,ValidatorOption option)
{
_jsonSerializer = jsonSerializer;
_dateTimeProvider = dateTimeProvider;
_option = option;
}
///
///
///
public void Validate(string payloadJson, string decodedCrypto, string decodedSignature)
{
var ex = GetValidationException(payloadJson, decodedCrypto, decodedSignature);
if (ex != null)
throw ex;
}
///
///
///
public void Validate(string payloadJson, string decodedCrypto, string[] decodedSignatures)
{
var ex = GetValidationException(payloadJson, decodedCrypto, decodedSignatures);
if (ex != null)
throw ex;
}
///
/// Given the JWT, verifies its signature correctness without throwing an exception but returning it instead
///
/// >An arbitrary payload (already serialized to JSON)
/// Decoded body
/// Decoded signature
/// Validation exception, if any
/// True if exception is JWT is valid and exception is null, otherwise false
public bool TryValidate(string payloadJson, string decodedCrypto, string decodedSignature, out Exception ex)
{
ex = GetValidationException(payloadJson, decodedCrypto, decodedSignature);
return ex is null;
}
///
/// Given the JWT, verifies its signatures correctness without throwing an exception but returning it instead
///
/// >An arbitrary payload (already serialized to JSON)
/// Decoded body
/// Decoded signatures
/// Validation exception, if any
/// True if exception is JWT is valid and exception is null, otherwise false
public bool TryValidate(string payloadJson, string decodedCrypto, string[] decodedSignature, out Exception ex)
{
ex = GetValidationException(payloadJson, decodedCrypto, decodedSignature);
return ex is null;
}
private Exception GetValidationException(string payloadJson, string decodedCrypto, string decodedSignature)
{
if (String.IsNullOrWhiteSpace(payloadJson))
return new ArgumentException(nameof(payloadJson));
if (String.IsNullOrWhiteSpace(decodedCrypto))
return new ArgumentException(nameof(decodedCrypto));
if (String.IsNullOrWhiteSpace(decodedSignature))
return new ArgumentException(nameof(decodedSignature));
if (!CompareCryptoWithSignature(decodedCrypto, decodedSignature))
return new SignatureVerificationException(decodedCrypto, decodedSignature);
return GetValidationException(payloadJson);
}
private Exception GetValidationException(string payloadJson, string decodedCrypto, string[] decodedSignatures)
{
if (String.IsNullOrWhiteSpace(payloadJson))
return new ArgumentException(nameof(payloadJson));
if (String.IsNullOrWhiteSpace(decodedCrypto))
return new ArgumentException(nameof(decodedCrypto));
if (AreAllDecodedSignaturesNullOrWhiteSpace(decodedSignatures))
return new ArgumentException(nameof(decodedSignatures));
if (!IsAnySignatureValid(decodedCrypto, decodedSignatures))
return new SignatureVerificationException(decodedCrypto, decodedSignatures);
return GetValidationException(payloadJson);
}
private Exception GetValidationException(string payloadJson)
{
if (!_option.ValidateLifetime)
{
return null;
}
var payloadData = _jsonSerializer.Deserialize>(payloadJson);
var now = _dateTimeProvider.GetNow();
var secondsSinceEpoch = UnixEpoch.GetSecondsSince(now);
return ValidateExpClaim(payloadData, secondsSinceEpoch) ?? ValidateNbfClaim(payloadData, secondsSinceEpoch);
}
private static bool AreAllDecodedSignaturesNullOrWhiteSpace(string[] decodedSignatures) =>
decodedSignatures.All(sgn => String.IsNullOrWhiteSpace(sgn));
private static bool IsAnySignatureValid(string decodedCrypto, string[] decodedSignatures) =>
decodedSignatures.Any(decodedSignature => CompareCryptoWithSignature(decodedCrypto, decodedSignature));
/// In the future this method can be opened for extension so made protected virtual
private static bool CompareCryptoWithSignature(string decodedCrypto, string decodedSignature)
{
if (decodedCrypto.Length != decodedSignature.Length)
return false;
var decodedCryptoBytes = Encoding.UTF8.GetBytes(decodedCrypto);
var decodedSignatureBytes = Encoding.UTF8.GetBytes(decodedSignature);
byte result = 0;
for (var i = 0; i < decodedCrypto.Length; i++)
{
result |= (byte) (decodedCryptoBytes[i] ^ decodedSignatureBytes[i]);
}
return result == 0;
}
///
/// Verifies the 'exp' claim.
///
/// See https://tools.ietf.org/html/rfc7515#section-4.1.4
///
///
private static Exception ValidateExpClaim(IDictionary payloadData, double secondsSinceEpoch)
{
if (!payloadData.TryGetValue("exp", out var expObj))
return null;
if (expObj is null)
return new SignatureVerificationException("Claim 'exp' must be a number.");
double expValue;
try
{
expValue = Convert.ToDouble(expObj);
}
catch
{
return new SignatureVerificationException("Claim 'exp' must be a number.");
}
if (secondsSinceEpoch >= expValue)
{
return new TokenExpiredException("Token has expired.");
}
return null;
}
///
/// Verifies the 'nbf' claim.
///
/// See https://tools.ietf.org/html/rfc7515#section-4.1.5
///
private static Exception ValidateNbfClaim(IReadOnlyDictionary payloadData,
double secondsSinceEpoch)
{
if (!payloadData.TryGetValue("nbf", out var nbfObj))
return null;
if (nbfObj is null)
return new SignatureVerificationException("Claim 'nbf' must be a number.");
double nbfValue;
try
{
nbfValue = Convert.ToDouble(nbfObj);
}
catch
{
return new SignatureVerificationException("Claim 'nbf' must be a number.");
}
if (secondsSinceEpoch < nbfValue)
{
return new SignatureVerificationException("Token is not yet valid.");
}
return null;
}
}
}