engineering
Debugging Invalid JWT Claims: Token Header, Payload, and Expiry Errors
Diagnose and fix invalid JWT claims. Learn how header, payload, and signature differ, solve exp, nbf, and iat timestamp errors, and troubleshoot issuer and audience mismatches safely.
Quick Answer: What Causes "Invalid Claims" Errors?
Invalid claim errors occur when a JSON Web Token (JWT) passes cryptographic signature checks but fails payload validation rules enforced by the verifying server or API gateway. The three most common causes are token expiration (exp timestamp in the past), clock drift between the authentication provider and the resource server without configured leeway, and mismatched identifier strings for the expected audience (aud) or issuer (iss).
To troubleshoot an invalid token:
1. Decode the token header and payload locally to inspect raw claim values.
2. Verify that all timestamp claims (exp, nbf, iat) are numeric Unix epoch timestamps in seconds, not milliseconds.
3. Compare the exp timestamp against your server's current system time in UTC (date -u +%s).
4. Ensure the iss and aud strings match the exact protocol, domain, and path expected by your validation middleware.
5. Inspect the token using the C³ MindLoop JWT Inspector to check expiration status and claim formatting with zero server transmission.
Understanding JWT Structure: Header, Payload, and Signature
A JSON Web Token (RFC 7519) is a compact, URL-safe string comprised of three distinct segments separated by periods (.):
header.payload.signature
- Header: Identifies the token type (
typ: "JWT") and the cryptographic signing algorithm (alg: "HS256","RS256", or"ES256"). It may also specify a key identifier (kid) used by the resource server to locate the corresponding public key in a JSON Web Key Set (JWKS). - Payload: Contains the claims—statements about an entity (typically the user or client) and additional metadata. Claims are stored as a standard JSON object before being base64url-encoded.
- Signature: Generated by applying the header's algorithm over the encoded header and encoded payload (
HMACSHA256(base64Url(header) + "." + base64Url(payload), secret)). The signature guarantees data integrity: if any byte in the header or payload is modified, the signature becomes invalid.
Standard Registered Claims & Common Failure Modes The JWT specification defines several standard registered claim keys. While optional in the RFC, modern authentication frameworks (OAuth 2.0, OpenID Connect) treat them as mandatory validation boundaries.
| Claim Key | Full Name | Expected Format | Common Failure Cause | Expected Verification Rule |
|---|---|---|---|---|
exp | Expiration Time | Unix Epoch (seconds) | Passed expiration, millisecond timestamps | currentTime <= exp + leeway |
nbf | Not Before | Unix Epoch (seconds) | Token received before activation time | currentTime >= nbf - leeway |
iat | Issued At | Unix Epoch (seconds) | Future timestamp due to clock skew | Informational only; most libraries do not enforce as a validity boundary |
iss | Issuer URL | String / URI | Trailing slash mismatch, wrong auth realm | token.iss === expectedIssuer |
aud | Audience | String or Array | Mismatched client ID or API identifier | expectedAudience in token.aud |
sub | Subject | String | Missing user ID or non-string format | Must be non-empty string identifier |
Unix Timestamps, UTC, and Time-Zone Mistakes
Timestamp miscalculations account for the vast majority of TokenExpiredError and NotBeforeError exceptions.
Seconds vs. Milliseconds
Standard Unix timestamps measure the number of elapsed seconds since January 1, 1970 00:00:00 UTC. However, JavaScript's Date.now() and Java's System.currentTimeMillis() return milliseconds.
- Incorrect generation: Setting "exp": Date.now() + 3600000 writes a 13-digit millisecond timestamp (e.g. 1730000000000). When the validating server reads this as seconds, it calculates an expiration date in the far distant future (over 50,000 years away) or fails integer bounds checks.
- Correct generation: Always divide by 1000 and truncate: Math.floor(Date.now() / 1000) + 3600.
- Incorrect client reading: Reading new Date(payload.exp) passes seconds to a constructor expecting milliseconds, producing a date in January 1970. Always multiply by 1000: new Date(payload.exp * 1000).
Timezone Independence and Clock Drift Unix epoch timestamps are inherently timezone-independent. They represent a single universal moment in time regardless of whether your server runs in UTC, EST, or JST. Do not apply timezone offsets (such as adding 3600 seconds for GMT+1) to an epoch calculation; doing so shifts the timestamp by an hour and produces premature expiration or delayed activation.
Even with synchronized UTC clocks, slight hardware drift between distributed cloud servers can cause a token minted on Server A to be rejected as "not active yet" (nbf) or "expired" (exp) on Server B. Verification libraries should always configure a small clock tolerance (typically 30 to 60 seconds):
// Node.js jsonwebtoken verification with clock tolerance
const jwt = require('jsonwebtoken');try { const decoded = jwt.verify(token, publicKey, { audience: 'https://api.c3mindloop.com', issuer: 'https://auth.c3mindloop.com', clockTolerance: 30, // 30 seconds leeway for clock skew }); } catch (err) { console.error('Validation failed:', err.message); } ```
# Python PyJWT verification with leeway
import jwttry: decoded = jwt.decode( token, public_key, algorithms=["RS256"], audience="https://api.c3mindloop.com", issuer="https://auth.c3mindloop.com", leeway=30 # 30 seconds leeway ) except jwt.ExpiredSignatureError: print("Token has expired") except jwt.InvalidAudienceError: print("Token audience mismatch") except jwt.PyJWTError as e: print(f"Token error: {e}") ```
Synthetic Troubleshooting Examples: Valid vs. Invalid Payloads The following synthetic examples illustrate valid configurations versus common syntax and logical mistakes.
1. Valid Token Payload
``json
{
"iss": "https://auth.c3mindloop.com",
"sub": "usr_98a72b1",
"aud": "https://api.c3mindloop.com",
"iat": 1729800000,
"nbf": 1729800000,
"exp": 1729803600,
"scope": "read:profile write:data"
}
``
Expected Outcome: Verification succeeds. The token is active, matches audience and issuer, and expires exactly 1 hour (3600 seconds) after issuance.
2. Invalid: Token Expired (TokenExpiredError)
``json
{
"iss": "https://auth.c3mindloop.com",
"sub": "usr_98a72b1",
"aud": "https://api.c3mindloop.com",
"iat": 1500000000,
"exp": 1500003600
}
`
Expected Outcome: Fails with TokenExpiredError: jwt expired. The exp value (1500003600`) represents July 14, 2017. The current timestamp exceeds the expiration time.
3. Invalid: Premature Token (NotBeforeError)
``json
{
"iss": "https://auth.c3mindloop.com",
"sub": "usr_98a72b1",
"aud": "https://api.c3mindloop.com",
"iat": 1729800000,
"nbf": 1729807200,
"exp": 1729810800
}
`
Expected Outcome: Fails with NotBeforeError: jwt not active. The nbf timestamp is set two hours after issuance. The resource server rejects access until the nbf` timestamp passes.
4. Invalid: Issuer or Audience Mismatch (JsonWebTokenError)
``json
{
"iss": "https://auth.c3mindloop.com/",
"sub": "usr_98a72b1",
"aud": "my-mobile-app",
"exp": 1999999999
}
`
Expected Outcome: Fails with JsonWebTokenError: jwt audience invalid if the resource server expects https://api.c3mindloop.com. Also note the trailing slash in iss: "https://auth.c3mindloop.com/" will NOT match an expected issuer string of "https://auth.c3mindloop.com"`. URL comparisons are exact string matches.
Security Rule: Decoding Is Not Verification
A critical security distinction often missed by developers is that base64url-decoding a JWT is NOT the same as verifying it.
- Any client, proxy, or browser can decode a JWT payload because base64url is an encoding format, not encryption.
- Never make access control, billing, or authorization decisions based solely on unverified decoded payloads.
- An attacker can easily craft an unverified payload containing "role": "admin" or an arbitrary "sub". Without signature verification using a trusted secret or verified public key, the payload is completely untrustworthy.
- Ensure your backend rejects tokens with "alg": "none" and verifies that the signing algorithm in the header matches the algorithm expected by your security policy.
Safe Inspection with C³ MindLoop JWT Inspector When inspecting tokens during development, never paste production credentials, private keys, or tokens containing sensitive personal data into unknown web servers.
Use the C³ MindLoop JWT Inspector to inspect tokens safely. The inspector operates 100% locally inside your browser's V8 engine:
- Decodes Header and Payload claims in-memory without sending network requests.
- Converts exp, nbf, and iat timestamps into human-readable local time and UTC with a live countdown timer.
- Scans for standard security warnings, including weak algorithms and missing expiration boundaries.
- Inspects token structure completely offline with zero server logs or retention.
JWT Inspector & Token Debugger
Decode JWT headers, payload claims, and expiration timestamps locally with zero server transmission.