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
Header
{
"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
| Claim | Description | Validation |
|---|---|---|
iss (Issuer) | Token issuer | Must match your Guardhouse domain |
aud (Audience) | Intended audience | Must match your client ID or API identifier |
exp (Expiration) | Token expiration | Must be in the future |
nbf (Not Before) | Token validity start | Must be in the past |
iat (Issued At) | Token issuance time | Must be reasonable |
sub (Subject) | User identifier | Must 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