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; } } }