JWT Signature Validation Disabled¶
What does this mean ?¶
Decoding a JWT reads its contents; verification establishes whether it satisfies a trusted signature and claims policy. A JWT payload is normally readable, even when signed. Applications must not trust a role or subject merely because the token's three segments decode correctly.
What can happen ?¶
A token accepted without proper verification may allow impersonation or misuse across services. A valid signature alone does not establish that the token was issued for this API, remains valid, or authorizes a particular resource operation.
Recommendation¶
Prefer a maintained authentication middleware configured against your identity provider. Fix the permitted signing algorithm and trusted issuer, audience and keys in application configuration. Never allow the token to select an arbitrary key URL or broaden the algorithm policy. For key rotation, obtain JWKS from the configured trusted issuer with a constrained cache and refresh policy.
Require the claims your contract needs, such as exp and sub, and validate expiration and nbf when present. Use a small justified clock tolerance. Restrict token type when several token classes share an issuer. Do not log bearer tokens, and apply record-level authorization after authentication.
Sample Code¶
These are verification examples for an API that accepts only RS256 tokens with the configured issuer and audience. publicKey, public_key and rsaPublicKey come from trusted service configuration. They are not extracted from an arbitrary URL inside the token. Error handlers should return a controlled authentication failure without echoing the token.
jsonwebtoken library:
import jwt from 'jsonwebtoken';
function verifyAccessToken(token, publicKey) {
const claims = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://identity.example.com/',
audience: 'reports-api',
clockTolerance: 5
});
if (typeof claims === 'string' || claims === null ||
typeof claims.exp !== 'number' || !Number.isFinite(claims.exp) ||
typeof claims.sub !== 'string' || claims.sub.length === 0) {
throw new Error('Required token claims missing');
}
return claims;
}
expiresIn belongs to jwt.sign, not jwt.verify. Verification checks an existing exp; the explicit presence check prevents accepting a token that omitted it. jwt.decode is not an authentication check.
PyJWT with its cryptographic dependency for RSA:
import jwt
def verify_access_token(token: str, public_key):
return jwt.decode(
token, public_key, algorithms=["RS256"],
issuer="https://identity.example.com/", audience="reports-api",
leeway=5,
options={"require": ["exp", "iss", "aud", "sub"]},
)
Enforce any application-specific subject format after this step. Do not set verify_signature=False for an authorization decision.
ASP.NET Core JWT bearer authentication with trusted configuration:
builder.Services.AddAuthentication(
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsaPublicKey),
ValidAlgorithms = new[] { "RS256" },
RequireSignedTokens = true,
ValidateIssuer = true,
ValidIssuer = "https://identity.example.com/",
ValidateAudience = true,
ValidAudience = "reports-api",
ValidateLifetime = true,
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromSeconds(5)
};
});
Import the authentication extension namespaces. Apply authentication/authorization middleware and a policy requiring your subject claim to the endpoints; configuring a handler alone does not protect routes.
Auth0 java-jwt, with a trusted RSAPublicKey:
var algorithm = com.auth0.jwt.algorithms.Algorithm.RSA256(rsaPublicKey, null);
var verifier = com.auth0.jwt.JWT.require(algorithm)
.withIssuer("https://identity.example.com/")
.withAudience("reports-api")
.withClaimPresence("exp")
.withClaimPresence("sub")
.acceptLeeway(5)
.build();
var verified = verifier.verify(token);
if (verified.getSubject() == null || verified.getSubject().isBlank()) {
throw new IllegalArgumentException("Subject required");
}
Dependency versions and identity-provider requirements must be reviewed together; do not mix methods from different JWT libraries.
Regression checks¶
Generate short-lived tokens using test-only keys in a local fixture. Accept the expected issuer/audience and reject a wrong key, wrong algorithm, wrong audience, expired token, future nbf, missing exp and missing subject. Test clock skew at its boundary. Confirm decoding alone is never used in an authorization path and that rejected tokens are not logged in full.