初始提交
This commit is contained in:
219
Infrastructure/Hncore.Infrastructure/WebApi/EtorJwtValidator.cs
Normal file
219
Infrastructure/Hncore.Infrastructure/WebApi/EtorJwtValidator.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="JwtValidator" />
|
||||
/// </summary>
|
||||
/// <param name="jsonSerializer">The Json Serializer</param>
|
||||
/// <param name="dateTimeProvider">The DateTime Provider</param>
|
||||
public EtorJwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimeProvider,ValidatorOption option)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_dateTimeProvider = dateTimeProvider;
|
||||
_option = option;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="ArgumentException" />
|
||||
/// <exception cref="SignatureVerificationException" />
|
||||
public void Validate(string payloadJson, string decodedCrypto, string decodedSignature)
|
||||
{
|
||||
var ex = GetValidationException(payloadJson, decodedCrypto, decodedSignature);
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="ArgumentException" />
|
||||
/// <exception cref="SignatureVerificationException" />
|
||||
public void Validate(string payloadJson, string decodedCrypto, string[] decodedSignatures)
|
||||
{
|
||||
var ex = GetValidationException(payloadJson, decodedCrypto, decodedSignatures);
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given the JWT, verifies its signature correctness without throwing an exception but returning it instead
|
||||
/// </summary>
|
||||
/// <param name="payloadJson">>An arbitrary payload (already serialized to JSON)</param>
|
||||
/// <param name="decodedCrypto">Decoded body</param>
|
||||
/// <param name="decodedSignature">Decoded signature</param>
|
||||
/// <param name="ex">Validation exception, if any</param>
|
||||
/// <returns>True if exception is JWT is valid and exception is null, otherwise false</returns>
|
||||
public bool TryValidate(string payloadJson, string decodedCrypto, string decodedSignature, out Exception ex)
|
||||
{
|
||||
ex = GetValidationException(payloadJson, decodedCrypto, decodedSignature);
|
||||
return ex is null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given the JWT, verifies its signatures correctness without throwing an exception but returning it instead
|
||||
/// </summary>
|
||||
/// <param name="payloadJson">>An arbitrary payload (already serialized to JSON)</param>
|
||||
/// <param name="decodedCrypto">Decoded body</param>
|
||||
/// <param name="decodedSignature">Decoded signatures</param>
|
||||
/// <param name="ex">Validation exception, if any</param>
|
||||
/// <returns>True if exception is JWT is valid and exception is null, otherwise false</returns>
|
||||
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<Dictionary<string, object>>(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));
|
||||
|
||||
/// <remarks>In the future this method can be opened for extension so made protected virtual</remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the 'exp' claim.
|
||||
/// </summary>
|
||||
/// <remarks>See https://tools.ietf.org/html/rfc7515#section-4.1.4</remarks>
|
||||
/// <exception cref="SignatureVerificationException" />
|
||||
/// <exception cref="TokenExpiredException" />
|
||||
private static Exception ValidateExpClaim(IDictionary<string, object> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the 'nbf' claim.
|
||||
/// </summary>
|
||||
/// <remarks>See https://tools.ietf.org/html/rfc7515#section-4.1.5</remarks>
|
||||
/// <exception cref="SignatureVerificationException" />
|
||||
private static Exception ValidateNbfClaim(IReadOnlyDictionary<string, object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user