Skip to main content

Validating Tokens (JWT)

Validating JSON Web Tokens (JWTs) is a critical security step in protecting your APIs. Guardhouse issues JWTs that must be validated before trusting their contents.

JWT Structure

A JWT consists of three parts separated by dots (.):

header.payload.signature

Example JWT

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ

Decoded Parts

{
"alg": "RS256",
"typ": "JWT"
}

Payload

{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}

Signature

The signature is created by signing the header and payload with a secret or private key.

Validation Steps

1. Verify Structure

Ensure the JWT has three parts separated by dots:

public static bool IsValidStructure(string token)
{
var parts = token.Split('.');
return parts.Length == 3;
}

2. Verify Signature

Validate that the token was signed by Guardhouse and hasn't been tampered with.

Public Key Discovery

Guardhouse exposes its public keys at the JWKS (JSON Web Key Set) endpoint:

https://your-tenant.guardhouse.cloud/.well-known/jwks.json

Signature Verification in .NET

using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;

public static async Task<ClaimsPrincipal> ValidateTokenAsync(string token, string issuer)
{
// Fetch the signing keys from Guardhouse
var httpClient = new HttpClient();
var jwksResponse = await httpClient.GetStringAsync($"{issuer}/.well-known/jwks.json");
var jwks = new JsonWebKeySet(jwksResponse);

var tokenHandler = new JwtSecurityTokenHandler();

var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudiences = new[] { "your-client-id" },
ValidateLifetime = true,
IssuerSigningKeys = jwks.Keys,
ClockSkew = TimeSpan.Zero // Remove default 5 minute tolerance
};

try
{
var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
return principal;
}
catch (SecurityTokenValidationException ex)
{
// Token validation failed
throw new UnauthorizedAccessException("Invalid token", ex);
}
}

3. Verify Standard Claims

Check essential claims to ensure the token is valid for your application.

Required Claims

ClaimDescriptionValidation
iss (Issuer)Token issuerMust match your Guardhouse domain
aud (Audience)Intended audienceMust match your client ID or API identifier
exp (Expiration)Token expirationMust be in the future
nbf (Not Before)Token validity startMust be in the past
iat (Issued At)Token issuance timeMust be reasonable
sub (Subject)User identifierMust be present and non-empty

Claim Validation in .NET

public static void ValidateClaims(ClaimsPrincipal principal, string expectedAudience)
{
var issuerClaim = principal.FindFirst("iss")?.Value;
var audienceClaim = principal.FindFirst("aud")?.Value;
var subClaim = principal.FindFirst("sub")?.Value;

if (string.IsNullOrEmpty(issuerClaim) || !issuerClaim.StartsWith("https://"))
{
throw new UnauthorizedAccessException("Invalid issuer claim");
}

if (string.IsNullOrEmpty(audienceClaim) || audienceClaim != expectedAudience)
{
throw new UnauthorizedAccessException("Invalid audience claim");
}

if (string.IsNullOrEmpty(subClaim))
{
throw new UnauthorizedAccessException("Missing subject claim");
}
}

4. Verify Custom Claims

Validate any custom claims or permissions required by your application:

public static void ValidatePermissions(ClaimsPrincipal principal, string requiredPermission)
{
var permissionClaim = principal.FindFirst("permissions")?.Value;

if (string.IsNullOrEmpty(permissionClaim))
{
throw new UnauthorizedAccessException("No permissions found");
}

var permissions = permissionClaim.Split(' ', ',');
if (!permissions.Contains(requiredPermission))
{
throw new UnauthorizedAccessException(
$"Permission '{requiredPermission}' not granted");
}
}

Middleware Implementation in .NET

Create a reusable middleware for token validation:

public class JwtAuthenticationMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;

public JwtAuthenticationMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}

public async Task InvokeAsync(HttpContext context)
{
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();

if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Missing or invalid Authorization header");
return;
}

var token = authHeader.Substring("Bearer ".Length).Trim();

try
{
var principal = await ValidateTokenAsync(token, _configuration["Guardhouse:Issuer"]);
var audience = _configuration["Guardhouse:Audience"];

ValidateClaims(principal, audience);

// Add user context to the request
context.User = principal;

await _next(context);
}
catch (Exception ex)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync($"Token validation failed: {ex.Message}");
}
}

private static async Task<ClaimsPrincipal> ValidateTokenAsync(string token, string issuer)
{
// Implementation from previous section
// ...
}
}

// Register middleware in Program.cs
app.UseMiddleware<JwtAuthenticationMiddleware>();

Token Validation Best Practices

1. Always Verify All Aspects

Never skip any validation step:

  • ✅ Verify signature with public keys
  • ✅ Verify issuer matches your domain
  • ✅ Verify audience matches your client ID
  • ✅ Verify expiration time
  • ✅ Verify required claims

2. Use JWKS for Key Rotation

Always fetch signing keys dynamically from the JWKS endpoint:

// Cache keys for performance, but refresh periodically
private static readonly ConcurrentDictionary<string, JsonWebKey> _keyCache = new();
private static DateTime _lastKeyFetch;

private static async Task<JsonWebKeySet> GetSigningKeysAsync(string issuer)
{
var now = DateTime.UtcNow;
if (_keyCache.IsEmpty || (now - _lastKeyFetch).TotalMinutes > 5)
{
var httpClient = new HttpClient();
var response = await httpClient.GetStringAsync($"{issuer}/.well-known/jwks.json");
var jwks = new JsonWebKeySet(response);

_keyCache.Clear();
foreach (var key in jwks.Keys)
{
_keyCache[key.Kid] = key;
}

_lastKeyFetch = now;
}

return new JsonWebKeySet(_keyCache.Values.Select(k => k).ToList());
}

3. Implement Proper Error Handling

Provide specific error messages for debugging, but not sensitive information:

public class TokenValidationError
{
public string Code { get; set; }
public string Message { get; set; }

public static TokenValidationError InvalidSignature() => new()
{
Code = "invalid_signature",
Message = "Token signature is invalid"
};

public static TokenValidationError Expired() => new()
{
Code = "token_expired",
Message = "Token has expired"
};

public static TokenValidationError InvalidIssuer() => new()
{
Code = "invalid_issuer",
Message = "Token issuer is not trusted"
};
}

4. Use Appropriate Token Lifetimes

  • Access Tokens: Short-lived (5-60 minutes)
  • Refresh Tokens: Longer-lived (days/weeks)
  • Balance security and user experience

5. Implement Caching

Cache validation results for performance, but respect token expiration:

public class TokenValidationCache
{
private readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 1000
});

public ClaimsPrincipal GetOrAdd(string token, Func<ClaimsPrincipal> factory)
{
return _cache.GetOrCreate(token, entry =>
{
var principal = factory();

// Set expiration based on token's exp claim
var expClaim = principal.FindFirst(JwtRegisteredClaimNames.Exp)?.Value;
if (long.TryParse(expClaim, out long exp))
{
var expiration = DateTimeOffset.FromUnixTimeSeconds(exp);
entry.AbsoluteExpiration = expiration;
}

return principal;
});
}
}

Introspection Endpoint

For additional security, you can use Guardhouse's introspection endpoint to validate tokens:

POST /oauth/introspect
Content-Type: application/x-www-form-urlencoded

token=your_access_token
&token_type_hint=access_token
&client_id=your_client_id
&client_secret=your_client_secret

Response:

{
"active": true,
"scope": "read:users write:users",
"client_id": "your_client_id",
"sub": "user_123456",
"exp": 1516239022,
"iat": 1516238722
}

Common Validation Errors

ErrorCauseSolution
Signature verification failedToken tampered or wrong keysVerify JWKS endpoint and keys
Token expiredToken past expiration timeRefresh token or re-authenticate
Invalid issuerWrong Guardhouse domainVerify issuer configuration
Invalid audienceWrong client ID or audienceVerify audience configuration
Missing required claimToken missing essential dataCheck token claims structure

For more information about tokens, see the Token Lifecycle section.