JWT Validation: Verifying Tokens for Authentication and Authorization
A practical guide to JWT validation — the process of checking a JSON Web Token's signature, claims, and structure to confirm a request is genuinely authenticated and authorized — covering signature verification, standard claim checks, key rotation, validation in ASP.NET Core, and the mistakes that most commonly lead to broken or bypassed validation.
Table of Contents
- Introduction
- Anatomy of a JWT
- Signing Algorithms
- What "Validation" Actually Checks
- Signature Verification and Key Rotation
- Standard Claim Validation
- Validating JWTs in ASP.NET Core
- Custom Validation Logic
- Token Revocation: JWT's Fundamental Limitation
- Validating JWTs Across Services
- Common Vulnerabilities
- Debugging Validation Failures
- Quick Reference Table
- Conclusion
Introduction
A JWT arriving in an Authorization: Bearer <token> header is just a string until it's actually validated — and validation is doing considerably more work than it might first appear. It's not just "does this look like a JWT" or even just "is the signature valid" — proper validation confirms the token was issued by a trusted party, intended for this specific API, still within its valid time window, and hasn't been tampered with in any way. Get any one of these checks wrong or skip it, and you can end up with an API that accepts tokens it absolutely shouldn't.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.Audience = "api://my-api";
});
Enter fullscreen mode Exit fullscreen mode
Those two lines look simple, but they configure a genuinely thorough validation pipeline underneath — this guide covers exactly what that pipeline actually checks, why each check matters, and where things commonly go wrong when validation is configured incorrectly or bypassed under pressure.
1. Anatomy of a JWT
Three parts, dot-separated
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGhzZXJ2ZXIuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImFwaTovL215LWFwaSIsImV4cCI6MTcyMTQwNDgwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└──────────────── header ────────────────┘└──────────────────────── payload ────────────────────────┘└──────── signature ────────┘
Enter fullscreen mode Exit fullscreen mode
The header
{ "alg": "RS256", "typ": "JWT", "kid": "abc123" }
Enter fullscreen mode Exit fullscreen mode
-
alg— the signing algorithm used (Section 2). -
typ— identifies this as a JWT. -
kid(key ID) — identifies which specific key (among potentially several currently valid ones) was used to sign this token, critical for key rotation (Section 4).
The payload (claims)
{
"iss": "https://authserver.com",
"sub": "1234567890",
"aud": "api://my-api",
"exp": 1721404800,
"iat": 1721401200,
"nbf": 1721401200,
"scp": "products.read products.write"
}
Enter fullscreen mode Exit fullscreen mode
The set of claims about the token, the subject it represents, and its validity window — covered in depth in Section 5.
The signature
signature = Sign(base64url(header) + "." + base64url(payload), private_key)
Enter fullscreen mode Exit fullscreen mode
A cryptographic signature computed over the header and payload together, using the issuer's private key — this is what lets a validator confirm the token genuinely came from the claimed issuer and that its content hasn't been altered since signing, using only the issuer's corresponding public key (Section 4).
Critically: the payload is NOT encrypted
echo "eyJpc3MiOiJodHRwczovL2F1dGhzZXJ2ZXIuY29tIn0" | base64 -d
# {"iss":"https://authserver.com"}
Enter fullscreen mode Exit fullscreen mode
A JWT's header and payload are merely base64url-encoded, not encrypted — anyone who intercepts a token (or a user simply inspecting their own token) can trivially decode and read every claim inside it. This is a frequent and consequential misunderstanding: a JWT provides integrity and authenticity (you can trust it hasn't been tampered with and genuinely came from the issuer) but provides no confidentiality at all. Never put genuinely sensitive data (passwords, secrets, or anything that shouldn't be visible to the token's bearer or anyone who intercepts it) directly into a JWT's claims.
2. Signing Algorithms
Asymmetric (the standard for OAuth2/OIDC access and ID tokens)
RS256 — RSA signature with SHA-256 (most common)
ES256 — ECDSA signature with SHA-256 (smaller signatures, growing adoption)
Enter fullscreen mode Exit fullscreen mode
With an asymmetric algorithm, the Authorization Server signs tokens using a private key it keeps secret, while any number of resource servers (APIs) can verify signatures using the corresponding public key — which the Authorization Server publishes openly (via the jwks_uri discovery endpoint covered in this series' OAuth2/OpenID Connect guide). This asymmetry is exactly what makes JWTs practical for distributed systems: dozens of independent APIs can all verify tokens without any of them ever holding a secret capable of issuing new valid tokens.
Symmetric (rarely appropriate for OAuth2/OIDC scenarios)
HS256 — HMAC with SHA-256, using a single shared secret for both signing AND verifying
Enter fullscreen mode Exit fullscreen mode
With a symmetric algorithm, the same secret both signs and verifies tokens — meaning every party that needs to verify a token must also hold a secret capable of creating valid tokens. This is fine for a single, self-contained application issuing and validating its own tokens internally, but it's a poor fit for the typical OAuth2/OIDC scenario of one Authorization Server and many independent resource servers, since distributing the signing secret to every API that needs to validate tokens means any of them could also forge tokens.
The alg: none attack — and why algorithm confirmation matters
{ "alg": "none", "typ": "JWT" }
Enter fullscreen mode Exit fullscreen mode
The JWT specification technically allows an alg value of "none", meaning no signature at all — a poorly implemented validator that blindly trusts whatever algorithm the token's header claims to use (rather than enforcing an expected algorithm) can be tricked into accepting a completely unsigned, freely-forgeable token. Section 10 covers this and a closely related attack (algorithm substitution) in more depth — the short version is: a validator must always enforce which specific algorithm(s) it expects, never simply trust the token's self-reported alg header.
3. What "Validation" Actually Checks
A properly implemented JWT validation process checks several genuinely distinct things, and skipping any one of them undermines the whole exercise:
1. Structural validity — is this actually a well-formed JWT (three base64url segments)?
2. Signature verification — was this signed by a key we trust, and is the content unaltered since signing?
3. Issuer (iss) — did this come from an Authorization Server we actually trust?
4. Audience (aud) — was this token issued specifically for THIS API, not some other one?
5. Expiration (exp) — has this token's validity window already passed?
6. Not-before (nbf) — is this token being used before it was even meant to become valid?
7. Algorithm confirmation — is the signing algorithm actually the one we expect, not attacker-chosen?
Enter fullscreen mode Exit fullscreen mode
Modern JWT libraries (Section 6) handle all seven automatically when configured correctly — but understanding each check individually matters both for correctly configuring a library and for recognizing when a custom or hand-rolled validation implementation is missing something important.
4. Signature Verification and Key Rotation
Fetching the public key
GET https://authserver.com/.well-known/jwks.json
Enter fullscreen mode Exit fullscreen mode
{
"keys": [
{
"kid": "abc123",
"kty": "RSA",
"use": "sig",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt...",
"e": "AQAB"
}
]
}
Enter fullscreen mode Exit fullscreen mode
The JWKS (JSON Web Key Set) endpoint publishes the current public key(s) an Authorization Server uses for signing — a validator fetches this document (typically cached for a reasonable interval, not on every single request) and uses the key matching the token's kid header to verify the signature.
Why key rotation exists, and why kid matters
Old key (kid: "abc123"): being phased out, still valid for tokens issued before rotation, until they naturally expire
New key (kid: "def456"): actively signing new tokens
Enter fullscreen mode Exit fullscreen mode
Authorization Servers periodically rotate their signing keys as a security best practice — limiting how long any single key remains in use, and providing a clean recovery path if a key were ever compromised. Because tokens signed with the old key remain valid until their own natural expiration, the JWKS endpoint typically publishes multiple currently-valid keys simultaneously during a rotation window, and the kid header in each token is what tells a validator exactly which of those published keys to use for that specific token.
Caching keys, but not forever
options.TokenValidationParameters.ConfigurationManager =
new ConfigurationManager<OpenIdConnectConfiguration>(metadataAddress, retriever)
{
AutomaticRefreshInterval = TimeSpan.FromHours(24),
};
Enter fullscreen mode Exit fullscreen mode
Fetching the JWKS document on every single incoming request would be wasteful and add unnecessary latency — validators cache the fetched keys for a reasonable period, but need to refresh periodically (and, ideally, be able to refresh immediately if a token references an unrecognized kid) so that a legitimate key rotation on the Authorization Server's side doesn't cause a wave of validation failures on the API side. Well-built OIDC client libraries (Section 6) handle this refresh logic automatically — this is exactly the kind of detail that's easy to get subtly wrong in a hand-rolled implementation.
5. Standard Claim Validation
Issuer (iss)
options.TokenValidationParameters.ValidIssuer = "https://login.microsoftonline.com/{tenant-id}/v2.0";
Enter fullscreen mode Exit fullscreen mode
Confirms the token was actually issued by an Authorization Server your application trusts — without this check, a validator that only verifies the signature could be tricked into accepting a perfectly validly-signed token from a different, untrusted issuer, if that issuer's public key were somehow available to check against (a real risk in multi-tenant or misconfigured scenarios, covered further in Section 10).
Audience (aud)
options.TokenValidationParameters.ValidAudience = "api://my-api";
Enter fullscreen mode Exit fullscreen mode
Confirms the token was issued specifically for this API, not for some other API or client application that happens to trust the same Authorization Server. This is one of the most consequential checks to get right — without it, a token legitimately issued for, say, a completely different internal API (but signed by the same trusted Authorization Server) could be replayed against your API and pass signature/issuer validation, since both checks would genuinely succeed; only the audience check catches this specific case.
Expiration (exp) and not-before (nbf)
exp: 1721404800 — token is invalid after this Unix timestamp
nbf: 1721401200 — token is invalid before this Unix timestamp (rare, but used for tokens issued for future use)
Enter fullscreen mode Exit fullscreen mode
Straightforward time-window checks — but worth knowing that most validation libraries apply a small clock skew tolerance (typically a few minutes) to accommodate minor clock drift between the issuing server and the validating server, rather than requiring perfectly synchronized clocks down to the second.
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5); // the .NET default
Enter fullscreen mode Exit fullscreen mode
An excessively generous clock skew tolerance meaningfully extends the practical window during which an expired (or not-yet-valid) token might still be accepted — worth deliberately tuning down for genuinely high-security scenarios rather than leaving an overly permissive default in place without consideration.
Subject (sub)
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
Enter fullscreen mode Exit fullscreen mode
Not typically "validated" against an expected value the way issuer/audience are, but this is the claim your application should use as the stable, canonical identifier for the authenticated user internally — never the email or username claim, which can change over a user's lifetime in ways sub generally does not.
6. Validating JWTs in ASP.NET Core
The JWT Bearer authentication handler
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.Audience = "api://my-api";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(2),
};
});
app.UseAuthentication();
app.UseAuthorization();
Enter fullscreen mode Exit fullscreen mode
Setting Authority is what triggers ASP.NET Core to automatically fetch the OIDC discovery document and JWKS endpoint (Section 4) and keep them refreshed — all seven of the checks from Section 3 are then performed automatically on every incoming request, without needing to hand-write any signature verification or claims-checking logic directly.
Requiring authentication on endpoints
app.MapGet("/products", () => GetProducts()).RequireAuthorization();
Enter fullscreen mode Exit fullscreen mode
[Authorize]
public class ProductsController : ControllerBase { }
Enter fullscreen mode Exit fullscreen mode
RequireAuthorization() (minimal APIs) or [Authorize] (MVC controllers) is what actually enforces that a valid, successfully-validated token must be present for a given endpoint — configuring the AddJwtBearer handler alone doesn't reject unauthenticated requests by itself; it needs to be paired with these authorization requirements on the specific endpoints that should demand it.
Explicitly enforcing the expected algorithm
options.TokenValidationParameters.ValidAlgorithms = new[] { "RS256" };
Enter fullscreen mode Exit fullscreen mode
As covered in Section 2 and Section 10, explicitly restricting which signing algorithms are acceptable (rather than trusting whatever the token's header claims) is a worthwhile, low-cost hardening step, particularly relevant if your validation code path is ever shared across multiple identity providers or configurations with potentially different algorithm expectations.
7. Custom Validation Logic
Adding application-specific checks beyond the standard ones
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var tenantId = context.Principal?.FindFirstValue("tid");
if (tenantId != _expectedTenantId)
{
context.Fail("Token issued for an unexpected tenant.");
return;
}
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
if (await _userService.IsUserSuspendedAsync(userId!))
{
context.Fail("User account is suspended.");
}
}
};
Enter fullscreen mode Exit fullscreen mode
The OnTokenValidated event fires after all the standard cryptographic and claim checks succeed, giving a hook for additional, application-specific validation — confirming a multi-tenant token actually belongs to the expected tenant, checking a user's current account status against your own database (something no amount of purely cryptographic token validation could know about), or enriching the resulting ClaimsPrincipal with additional claims looked up from elsewhere.
Why this matters: cryptographic validity isn't the same as "should this request be allowed"
A token can pass every check in Section 3 — genuinely signed by a trusted issuer, correct audience, not expired — and still represent a request that shouldn't be honored, because the user's account was suspended five minutes after the token was issued, or because of some other business rule the Authorization Server has no knowledge of. Custom validation logic is exactly where these gaps get closed, and it's worth deliberately considering what application-specific checks belong here rather than assuming "the token validated" is equivalent to "this request should be allowed."
8. Token Revocation: JWT's Fundamental Limitation
The core tension
JWTs are validated statelessly — that's their whole appeal, letting an API verify a token's authenticity locally without a network round-trip back to the Authorization Server on every request (as covered in this series' OAuth2/OpenID Connect guide). But this same property means a JWT, once issued, remains cryptographically valid until its natural expiration, even if the Authorization Server would very much like to revoke it right now (a user logged out, an admin disabled an account, a token was detected as stolen).
Token issued, exp: 1 hour from now
5 minutes later: user's account is suspended
...but the token remains cryptographically valid for the remaining 55 minutes, unless additional measures are taken
Enter fullscreen mode Exit fullscreen mode
Mitigation 1: keep access token lifetimes short
Access token lifetime: 15 minutes (a common, reasonable default)
Enter fullscreen mode Exit fullscreen mode
The most common and simplest mitigation is simply not letting this window be very large in the first place — a short-lived access token limits how long a "can no longer be trusted, but hasn't technically expired" token remains usable, at the cost of more frequent (though invisible to the user, handled via refresh tokens as covered in the OAuth2/OpenID Connect guide) token renewal.
Mitigation 2: a revocation/deny list checked in custom validation
OnTokenValidated = async context =>
{
var jti = context.Principal?.FindFirstValue("jti"); // unique token identifier
if (await _revocationStore.IsRevokedAsync(jti!))
{
context.Fail("Token has been revoked.");
}
}
Enter fullscreen mode Exit fullscreen mode
For scenarios where waiting out even a short expiration window is unacceptable (immediately revoking access after a detected account compromise, for instance), maintaining an explicit revocation list — checked via the token's unique jti (JWT ID) claim during custom validation (Section 7) — reintroduces exactly the stateful, per-request check that JWTs were designed to avoid, but only for this specific, narrower need, rather than for every claim in the token.
Mitigation 3: opaque tokens for genuinely instant revocation needs
As covered in this series' OAuth2/OpenID Connect guide, an opaque token (validated via a call back to the Authorization Server's introspection endpoint on every request) sidesteps this limitation entirely, since the Authorization Server can simply stop recognizing a revoked token immediately — at the direct cost of reintroducing the network round-trip and Authorization-Server-availability dependency that JWTs exist specifically to avoid. Choosing between JWTs and opaque tokens is, in large part, a choice about exactly this tradeoff: local, fast, stateless validation with a bounded revocation delay, versus centralized, instantly revocable validation with a per-request dependency.
9. Validating JWTs Across Services
The gRPC/microservices scenario
// Each internal service validates the JWT independently, using the same shared Authority
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://internal-idp.mycompany.com";
options.Audience = "api://internal-services";
});
Enter fullscreen mode Exit fullscreen mode
In a microservices architecture (connecting to this series' gRPC and Background Services guides), each individual service can independently validate an incoming JWT using the same shared Authorization Server's public keys — no service needs to trust any other service directly, or maintain its own separate credential store; they all validate against the same central source of truth, which is one of the properties that makes JWT-based authentication attractive for internal service-to-service communication at scale.
Token propagation vs. re-issuance between services
Client → API Gateway (validates the user's token) → Service A (re-validates? or trusts the gateway?)
Enter fullscreen mode Exit fullscreen mode
A design decision worth making deliberately: does an internal service re-validate a token that's already passed through an API gateway or another upstream service, or does it trust that upstream validation already happened? Re-validating at every hop is more defensive (protecting against a compromised or misconfigured upstream service) but adds latency and complexity; trusting an upstream gateway's validation is simpler but concentrates trust (and risk) at that gateway. Many organizations use a service mesh (as mentioned in this series' Kubernetes/Helm guide) with mutual TLS between services specifically so that internal service-to-service calls carry their own transport-level trust, somewhat independent of whether every hop separately re-validates the original user's JWT.
On-Behalf-Of flows for service chains
User's token → Service A exchanges it for a new token scoped to call Service B, on the user's behalf
Enter fullscreen mode Exit fullscreen mode
For a request chain where an intermediate service needs to call a downstream service as the original user (not just as itself), OAuth2's On-Behalf-Of flow lets a service exchange an incoming token for a new one scoped appropriately for the next hop — preserving the original user's identity through the chain rather than the intermediate service simply using its own service-to-service credentials and losing track of who the original request was actually for.
10. Common Vulnerabilities
The alg: none and algorithm confusion attacks
// ❌ Vulnerable: blindly trusting the token's self-declared algorithm
var algorithm = tokenHeader["alg"];
// if algorithm == "none", skip signature check entirely — catastrophic
// ✅ Safe: the validator enforces an explicit, expected algorithm regardless of what the token claims
options.TokenValidationParameters.ValidAlgorithms = new[] { "RS256" };
Enter fullscreen mode Exit fullscreen mode
Beyond the alg: none attack mentioned in Section 2, a related algorithm confusion attack targets validators that support both symmetric (HS256) and asymmetric (RS256) algorithms: an attacker who knows an RS256 public key can sometimes craft a token signed with HS256 using that public key as the HMAC secret — if the validator doesn't strictly enforce which specific algorithm it expects, it might mistakenly verify the forged HS256 signature using what it treats as a shared secret, when that "secret" was actually a publicly known value all along. Modern, well-maintained JWT libraries guard against both of these by default, but it's exactly the kind of subtle issue that makes hand-rolling JWT validation logic risky compared to using an established, actively maintained library.
Missing audience validation
// ❌ Only validates signature and issuer — accepts tokens meant for a completely different API
options.TokenValidationParameters.ValidateAudience = false;
Enter fullscreen mode Exit fullscreen mode
As covered in Section 5, skipping audience validation means a token legitimately issued for a different API (but by the same trusted Authorization Server) can be replayed against yours — this is a genuinely common misconfiguration, sometimes introduced while debugging a validation issue ("let's just disable this check to get it working") and never re-enabled afterward.
Trusting client-supplied claims without server-side verification
// ❌ Never trust a role/permission claim that a CLIENT could have supplied unsigned/unverified
var isAdmin = Request.Headers["X-User-Role"] == "admin";
// ✅ Only trust claims that came from the cryptographically verified token itself
var isAdmin = User.HasClaim("role", "admin");
Enter fullscreen mode Exit fullscreen mode
Any authorization decision needs to be based on claims from the actual verified, signed JWT — never from a separate, unsigned header or parameter a client could simply set to whatever value it wants. This sounds obvious in isolation but is a genuinely common mistake in systems that evolved incrementally, where a "temporary" debug header or client-supplied parameter quietly ends up influencing a real authorization decision.
Not validating token type/purpose
// A refresh token, ID token, and access token can all be JWTs with a similar shape —
// nothing stops a client from mistakenly (or maliciously) sending the wrong one to an endpoint expecting a specific type
Enter fullscreen mode Exit fullscreen mode
As touched on in this series' OAuth2/OpenID Connect guide, ID tokens and access tokens serve different purposes and typically have different intended audiences — but if an API doesn't specifically check that an incoming token's claims match what it expects for its own purpose (correct audience, expected scopes present), it risks accepting a token that was never actually intended for that purpose in the first place, even if the token is otherwise perfectly validly signed.
Overly permissive ValidIssuers/ValidAudiences in multi-tenant scenarios
// ❌ Accepts a token from ANY tenant under this identity provider, not just yours
options.TokenValidationParameters.ValidIssuer = null;
options.TokenValidationParameters.ValidateIssuer = false;
Enter fullscreen mode Exit fullscreen mode
In a multi-tenant SaaS scenario built on a shared identity provider (like Microsoft Entra ID's multi-tenant app registrations), disabling or overly broadening issuer validation to "make multi-tenancy work" can inadvertently allow tokens from any tenant's users to authenticate against your application, not just tenants that have actually been provisioned/consented for it — multi-tenant validation needs a deliberate, explicit allowlist of valid tenant issuers (or an equivalent tenant-ID claim check in custom validation, Section 7), not simply disabling the check.
11. Debugging Validation Failures
Reading the actual failure reason
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
_logger.LogWarning(context.Exception, "JWT validation failed");
return Task.CompletedTask;
}
};
Enter fullscreen mode Exit fullscreen mode
By default, ASP.NET Core's JWT Bearer handler doesn't surface a detailed reason to the client (a good security default — you generally don't want to tell an attacker exactly why their forged token was rejected), but logging the actual exception server-side is essential for diagnosing legitimate validation problems during development and in production incident response.
Common failure reasons and what they indicate
| Error | Likely cause |
|---|---|
IDX10223: Lifetime validation failed. The token is expired |
Access token naturally expired — client should have refreshed it |
IDX10214: Audience validation failed |
Token was issued for a different API/client than the one validating it |
IDX10205: Issuer validation failed |
Token came from an unexpected or untrusted Authorization Server |
IDX10501: Signature validation failed. Unable to match key |
kid in the token doesn't match any currently-cached key — possibly a key rotation the validator hasn't refreshed yet |
IDX10223-adjacent clock-related errors |
Clock skew between the issuing and validating servers exceeding the configured tolerance |
Decoding a token manually for inspection (never for production validation)
# Splitting and base64-decoding the payload segment, purely for human inspection during debugging
echo "<payload-segment>" | base64 -d | jq
Enter fullscreen mode Exit fullscreen mode
Manually decoding a token's claims (via a tool like jwt.io, or a quick shell one-liner) is a useful, purely diagnostic step for confirming what claims a token actually contains during debugging — this is emphatically not a substitute for actual cryptographic validation in application code, and no production code path should ever treat a merely-decoded, unverified token as trustworthy.
Quick Reference Table
| Concept | Purpose |
|---|---|
| Header / payload / signature | The three components of a JWT |
kid |
Identifies which specific signing key was used, for key rotation support |
RS256/ES256 (asymmetric) |
Standard for OAuth2/OIDC — one issuer signs, many verifiers check |
HS256 (symmetric) |
Shared-secret signing, generally unsuitable for multi-party OAuth2 scenarios |
iss validation |
Confirms the token came from a trusted Authorization Server |
aud validation |
Confirms the token was issued specifically for this API |
exp/nbf validation |
Confirms the token is within its intended validity window |
| JWKS endpoint | Publishes the public key(s) needed to verify signatures |
OnTokenValidated |
Hook for custom, application-specific validation beyond standard checks |
Revocation list / jti
|
Reintroduces stateful checking for scenarios needing instant revocation |
| Algorithm confusion attack | Exploiting a validator that doesn't strictly enforce the expected signing algorithm |
Conclusion
JWT validation looks deceptively simple from the outside — check a signature, read some claims — but a properly implemented validator is quietly doing seven distinct, individually important checks, several of which (audience validation, algorithm enforcement, issuer restriction in multi-tenant scenarios) are exactly the checks that get accidentally weakened or disabled under debugging pressure and never restored. The practical guidance is consistent with the rest of this series' security-adjacent guides: use an established, actively maintained library (ASP.NET Core's AddJwtBearer, backed by Microsoft.IdentityModel.Tokens) rather than hand-rolling signature verification or claims checking, let it handle key rotation and discovery automatically via Authority, and use its extension points (OnTokenValidated) for the genuinely application-specific checks — like account status or tenant scoping — that no amount of purely cryptographic validation could ever know about on its own.
Understanding what's actually happening underneath those two or three lines of configuration is what turns "the auth middleware is throwing an error" from a mystery into a solvable problem, and what makes it possible to recognize when a seemingly reasonable-looking validation configuration has quietly disabled a check it never should have.
Found this useful? Feel free to star the repo, open an issue with corrections, or share the audience-validation bug that taught you to never disable a check just to make an error go away.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.