Lightweight .NET Framework 4.7.2 library for validating JWT expiration, claims (iss, aud, nbf, iat), and verifying signatures (HS256, RS256). No dependencies. Well-tested, production-ready, and CI/CD enabled.
$ dotnet add package ValidateJWTA lightweight .NET Framework 4.7.2 library for validating JWT (JSON Web Token) expiration times with optional signature verification support.
ValidateJWT is built as AnyCPU and works on both x86 and x64 platforms:
Install-Package ValidateJWT
dotnet add package ValidateJWT
<PackageReference Include="ValidateJWT" Version="1.1.0" />
using Johan.Common;
var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."";
// Check if token is expired
if (ValidateJWT.IsExpired(token))
{
Console.WriteLine("Token has expired");
}
// Check if token is currently valid
if (ValidateJWT.IsValidNow(token))
{
Console.WriteLine("Token is valid");
}
// Get expiration time
DateTime? expiration = ValidateJWT.GetExpirationUtc(token);
Console.WriteLine($"Expires: {expiration}");
using Johan.Common;
var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
var secret = "your-secret-key";
// Verify signature with HS256
var result = ValidateJWT.VerifySignature(token, secret);
if (result.IsValid && !result.IsExpired)
{
Console.WriteLine("? Token is valid and not expired");
}
else if (!result.IsValid)
{
Console.WriteLine($"? Invalid signature: {result.ErrorMessage}");
}
else if (result.IsExpired)
{
Console.WriteLine("? Token has expired");
}
IsExpired(jwt, clockSkew, nowUtc)Checks if a JWT token has expired.
bool isExpired = ValidateJWT.IsExpired(token);
bool isExpired = ValidateJWT.IsExpired(token, TimeSpan.FromMinutes(10));
Parameters:
jwt (string) - JWT tokenclockSkew (TimeSpan?) - Clock skew tolerance (default: 5 minutes)nowUtc (DateTime?) - Current UTC time (default: DateTime.UtcNow)Returns: bool - True if expired, false otherwise
IsValidNow(jwt, clockSkew, nowUtc)Checks if a JWT token is currently valid.
bool isValid = ValidateJWT.IsValidNow(token);
Returns: bool - True if valid, false if expired or invalid
GetExpirationUtc(jwt)Extracts the expiration time from a JWT token.
DateTime? expiration = ValidateJWT.GetExpirationUtc(token);
Returns: DateTime? - Expiration time in UTC, or null if not found
VerifySignature(jwt, secretKey)Verifies JWT signature using HMAC-SHA256 (HS256).
var result = ValidateJWT.VerifySignature(token, "your-secret-key");
if (result.IsValid && !result.IsExpired)
{
// Token is fully validated
}
Parameters:
jwt (string) - JWT tokensecretKey (string) - Secret key used to sign the tokenReturns: JwtVerificationResult
IsValid (bool) - Whether signature is validAlgorithm (string) - Algorithm used (e.g., "HS256")ErrorMessage (string) - Error details if failedIsExpired (bool) - Whether token is expiredVerifySignatureRS256(jwt, publicKeyXml)Verifies JWT signature using RSA-SHA256 (RS256).
var publicKey = "<RSAKeyValue>...</RSAKeyValue>";
var result = ValidateJWT.VerifySignatureRS256(token, publicKey);
Parameters:
jwt (string) - JWT tokenpublicKeyXml (string) - RSA public key in XML formatReturns: JwtVerificationResult
GetAlgorithm(jwt) ??Gets the algorithm from the JWT header.
string algorithm = ValidateJWT.GetAlgorithm(token);
// Returns: "HS256", "RS256", etc.
IsIssuerValid(jwt, expectedIssuer)Validates the 'iss' (issuer) claim in a JWT token.
bool isIssuerValid = ValidateJWT.IsIssuerValid(token, "https://my-issuer.example.com");
Returns: bool - True if the issuer matches, false otherwise or if the claim is missing/invalid.
IsAudienceValid(jwt, expectedAudience)Validates the 'aud' (audience) claim in a JWT token. Supports both single audience and audience arrays.
bool isAudienceValid = ValidateJWT.IsAudienceValid(token, "my-api");
Returns: bool - True if the audience matches, false otherwise or if the claim is missing/invalid.
IsNotBeforeValid(jwt, clockSkew, nowUtc)Validates the 'nbf' (not before) claim in a JWT token.
bool isNotBeforeValid = ValidateJWT.IsNotBeforeValid(token);
bool isNotBeforeValid = ValidateJWT.IsNotBeforeValid(token, TimeSpan.FromMinutes(2));
Parameters:
jwt (string) - JWT tokenclockSkew (TimeSpan?) - Clock skew tolerance (default: 5 minutes)nowUtc (DateTime?) - Current UTC time (default: DateTime.UtcNow)Returns: bool - True if the token is not being used before its 'nbf' time, false otherwise.
GetNotBeforeUtc(jwt)Extracts the 'nbf' (not before) time from a JWT token.
DateTime? notBefore = ValidateJWT.GetNotBeforeUtc(token);
Returns: DateTime? - Not before time in UTC, or null if not found.
GetIssuedAtUtc(jwt)Extracts the 'iat' (issued at) time from a JWT token.
DateTime? issuedAt = ValidateJWT.GetIssuedAtUtc(token);
Returns: DateTime? - Issued at time in UTC, or null if not found.
GetAudience(jwt)Extracts the 'aud' (audience) claim from a JWT token.
string audience = ValidateJWT.GetAudience(token);
Returns: string - Audience value, or null if not found. For audience arrays, returns the first audience.
Base64UrlDecode(input)Decodes Base64URL encoded strings.
byte[] decoded = ValidateJWT.Base64UrlDecode("SGVsbG8gV29ybGQ");
Base64UrlEncode(input) ??Encodes byte arrays to Base64URL format.
byte[] data = Encoding.UTF8.GetBytes("Hello");
string encoded = ValidateJWT.Base64UrlEncode(data);
// Fast pre-check before expensive operations
if (ValidateJWT.IsExpired(token))
{
return Unauthorized("Token expired");
}
// Proceed with API call
// Full signature and expiration validation
var result = ValidateJWT.VerifySignature(token, secretKey);
if (!result.IsValid)
{
return Unauthorized($"Invalid token: {result.ErrorMessage}");
}
if (result.IsExpired)
{
return Unauthorized("Token expired");
}
// Token is fully validated
// Stage 1: Quick time check (0.1ms)
if (ValidateJWT.IsExpired(token))
{
return Unauthorized("Token expired");
}
// Stage 2: Signature verification (0.5-5ms)
var result = ValidateJWT.VerifySignature(token, secretKey);
if (!result.IsValid)
{
return Unauthorized("Invalid signature");
}
// Token is valid
var algorithm = ValidateJWT.GetAlgorithm(token);
JwtVerificationResult result;
switch (algorithm)
{
case "HS256":
result = ValidateJWT.VerifySignature(token, secretKey);
break;
case "RS256":
result = ValidateJWT.VerifySignatureRS256(token, publicKey);
break;
default:
return Unauthorized($"Unsupported algorithm: {algorithm}");
}
if (result.IsValid && !result.IsExpired)
{
// Token is valid
}
Account for time synchronization issues between servers:
// Default: 5 minutes
bool isExpired = ValidateJWT.IsExpired(token);
// Custom: 10 minutes
bool isExpired = ValidateJWT.IsExpired(token, TimeSpan.FromMinutes(10));
// No clock skew
bool isExpired = ValidateJWT.IsExpired(token, TimeSpan.Zero);
Inject custom time for deterministic testing:
var testTime = new DateTime(2025, 1, 15, 12, 0, 0, DateTimeKind.Utc);
bool isExpired = ValidateJWT.IsExpired(token, null, testTime);
iss) claim validationaud) claim validationnbf) claim validation (planned for v1.2)Always verify signatures in production:
var result = ValidateJWT.VerifySignature(token, secret);
if (!result.IsValid) return Unauthorized();
Store secrets securely:
// ? Good - from configuration
var secret = _configuration["JWT:Secret"];
// ? Bad - hardcoded
var secret = "my-secret-123";
Use appropriate algorithm:
Combine with full JWT validation:
// Step 1: Quick expiration check
if (ValidateJWT.IsExpired(token)) return Unauthorized();
// Step 2: Signature verification
var result = ValidateJWT.VerifySignature(token, secret);
if (!result.IsValid) return Unauthorized();
// Step 3: Validate claims (issuer, audience, etc.)
// ... your claim validation logic
Comprehensive test suite with 58+ unit tests:
// Test expiration
[TestMethod]
public void IsExpired_ExpiredToken_ReturnsTrue()
{
var token = CreateExpiredToken();
Assert.IsTrue(ValidateJWT.IsExpired(token));
}
// Test signature verification
[TestMethod]
public void VerifySignature_ValidToken_ReturnsTrue()
{
var result = ValidateJWT.VerifySignature(validToken, secret);
Assert.IsTrue(result.IsValid);
}
Test Coverage:
IsExpired()IsValidNow()GetExpirationUtc()Base64UrlDecode()| Operation | Time | Notes |
|---|---|---|
| Time Check | ~0.1ms | Very fast |
| HS256 Verify | ~0.5-1ms | HMAC verification |
| RS256 Verify | ~2-5ms | RSA verification (slower) |
Optimization tip: Check expiration first, then verify signature.
IsAudienceValid() for 'aud' claim validation (supports single and array formats)IsNotBeforeValid() for 'nbf' claim validationGetNotBeforeUtc() for extracting 'nbf' timestampsGetIssuedAtUtc() for extracting 'iat' timestampsGetAudience() for extracting 'aud' claimsIsIssuerValid() for 'iss' claim validationVerifySignature() and VerifySignatureRS256() methodsJwtVerificationResult classGetAlgorithm() helperBase64UrlEncode() helperSee CHANGELOG.md for complete history.
ValidateJWT includes a complete CI/CD pipeline:
Testing:
# Run all tests with coverage
.\Run-AutomatedTests.ps1 -GenerateCoverage
# Fix and run tests
.\Fix-And-RunTests.ps1
Publishing:
# Build NuGet package
.\BuildNuGetPackage.bat
# Publish to NuGet.org
.\Publish-NuGet.ps1 -Version "1.1.0"
Maintenance:
# Remove unused references
.\Remove-UnusedReferences.ps1
# Clean company references
.\Remove-CompanyReferences.ps1
# Setup CI/CD
.\Setup-CICD.ps1
See CI_CD_GUIDE.md for complete automation documentation.
Contributions are welcome! Please:
git checkout -b feature/amazing-feature).\Run-AutomatedTests.ps1)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)# Clone repository
git clone https://github.com/johanhenningsson4-hash/ValidateJWT.git
cd ValidateJWT
# Restore packages
nuget restore ValidateJWT.sln
# Build
msbuild ValidateJWT.sln /p:Configuration=Release
# Run tests
.\Run-AutomatedTests.ps1
For questions, issues, or feature requests:
Made with ?? for the .NET community
Author: Johan Henningsson
Version: 1.1.0
Framework: .NET Framework 4.7.2
Last Updated: January 2026
Status: ? Production-Ready | ? CI/CD Enabled | ? Fully Automated
To build the library and run tests, always use the platform string AnyCPU (no space):
msbuild ValidateJWT.sln /p:Configuration=Release /p:Platform=AnyCPU
If you use AnyCPU (with a space), you may get an error about BaseOutputPath/OutputPath property is not set.
Troubleshooting:
BaseOutputPath/OutputPath property is not set, check that you are using AnyCPU (no space) for the platform.AnyCPU, x64, and x86 (if defined in the project file).