Receiving a JWT and successfully base64-decoding its payload is not the same as validating it — and skipping real validation is one of the most common, serious mistakes in systems built around tokens. A correct verification step checks all of the following, not just "does the signature match something":
1. Signature — recompute it using the EXPECTED algorithm and key/secret (never the
algorithm the token itself claims — see the alg-confusion attack above), and
reject the token outright if it doesn't match.
2. exp — reject if the current time is at or past this token's expiration.
3. nbf — reject if the current time is before this token is allowed to be used.
4. iss — reject if this token wasn't issued by the authorization server you actually trust.
5. aud — reject if this token wasn't intended for your application/API specifically
(a token legitimately issued for a DIFFERENT audience should never be accepted
just because it happens to be validly signed by an issuer you trust).
// A realistic ASP.NET Core JWT bearer validation configuration — note that
// every check above is an explicit, named parameter, not implied behavior.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://accounts.google.com",
ValidateAudience = true,
ValidAudience = "your-client-id",
ValidateLifetime = true, // enforces exp and nbf
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey, // the EXPECTED key — never derived from the token
ValidAlgorithms = new[] { "RS256" } // pin the algorithm explicitly
};
});
Skipping aud validation is a specific, realistic bug: a company running several internal APIs behind one shared identity provider can end up with a token legitimately issued for "the reporting service" being accepted by "the billing service" too, simply because both trust the same issuer and neither checks who the token was actually meant for.