Published on January 28, 2026
Decoding a JWT extracts the header and payload. It does not prove the token is valid or trustworthy. This distinction is critical. Many security bugs come from treating decoded claims as if they were verified. Decoding is a simple Base64URL decode operation. Anyone can decode a JWT without secrets or keys. The decoded payload might claim "role": "admin" but that does not mean it is true. Always verify signatures before trusting claims.
Verification checks the signature using the public key or secret. Only after verification passes should you trust the claims inside the token. Verification must happen server-side in code you control, not in client JavaScript. Client-side verification is security theater. JavaScript running in the browser is untrusted code. Attackers control the browser and can modify any client-side checks. Verification must happen on your servers where attackers cannot tamper with code.
Debugging tools that decode JWTs are safe for inspection but dangerous for authorization decisions. Use them to see what claims are present, check expiry timestamps, or validate issuer fields. Never use decoded data to grant access. Tools like jwt.io let you paste tokens and see decoded content. This is useful for understanding token structure during development. But production authorization must use proper verification libraries that check signatures.
The JWT structure has three parts: header, payload, and signature. Each part is Base64URL encoded and separated by dots. Understanding this format helps when inspecting raw tokens. The structure is: header.payload.signature. You can manually decode each part with Base64URL decoding to see contents. The signature is binary data that binds the header and payload together cryptographically.
Header contains algorithm and token type. Common algorithms include HS256 (HMAC with SHA-256) and RS256 (RSA with SHA-256). Algorithm choice affects verification process. HS256 uses a shared secret that both signer and verifier must know. RS256 uses public key cryptography where the signer has a private key and verifiers have a public key. RS256 is better for distributed systems because verifiers do not need access to signing secrets.
Payload contains claims. Standard claims like iss (issuer), sub (subject), exp (expiration), and aud (audience) have defined meanings. Custom claims extend functionality but should follow naming conventions. Registered claims have short names to keep token size down. Iss identifies who issued the token. Sub identifies the subject (usually user ID). Exp is Unix timestamp when token expires. Aud specifies intended audience. Custom claims should use namespaced names to avoid collisions.
Signature binds header and payload together. Without signature verification, anyone can modify claims and create fake tokens. This is why client-side decode is inspection only. The signature is computed by taking the Base64URL encoded header and payload, concatenating them with a dot, then signing with the algorithm specified in the header. Verification recomputes this signature and compares it to the signature in the token. If they match, the token is valid and unmodified.
None algorithm is a security anti-pattern. Some implementations accept "alg": "none" and skip verification. This must be explicitly disabled in production code. The none algorithm was intended for unsecured JWTs used in testing. But attackers discovered they could modify tokens to use "alg": "none" and some verifiers would accept them without checking signatures. All production JWT libraries should reject none algorithm. Check your library documentation and test that none tokens are rejected.
Token debugging should happen in controlled environments. Use development or staging tokens, not production credentials. Production tokens contain real user data and credentials. If you need to debug a production issue, generate a test token with similar claims but dummy data. Never copy real user tokens from production logs into development tools or share them in bug reports. Token leakage is a serious security incident.
JWT decoding is deterministic. The same token always decodes to the same header and payload. But verification can fail for many reasons: wrong secret, expired token, clock skew, modified claims. Understanding what can fail helps debug auth issues.
Token introspection is another debugging tool. OAuth provides introspection endpoints that let you check token validity without seeing the signature. This is useful for debugging opaque tokens or checking revocation status. Introspection is an API call to the authorization server that returns token metadata and validity.
Logging decoded token claims (minus signature) helps debug without exposing secrets. Log user ID, expiration, issuer, but never log signatures or refresh tokens. This gives you visibility into auth flows without creating security risks.
Token parsers should be strict. Lenient parsing can hide bugs or security issues. Enforce that tokens have exactly three parts separated by dots. Validate Base64URL encoding. Check that header and payload are valid JSON. Reject tokens that do not meet strict format requirements. Strictness prevents exploitation of parser ambiguities.
Public claims should use collision-resistant names. If multiple systems add custom claims, name conflicts cause bugs. Use URIs or reverse domain notation: "https://example.com/claims/department" or "com.example.claims.department". This prevents conflicts across different token issuers.
Expired tokens are the most frequent auth issue. Check the exp claim and compare it to current Unix timestamp. If the token expired, the solution is token refresh, not bypassing expiry checks. Many developers see expired token errors and temporarily extend expiration to "fix" the problem. This masks the real issue, which is usually missing or broken refresh logic. Implement proper token refresh flows so users stay logged in seamlessly. Short-lived access tokens with longer-lived refresh tokens is the standard pattern.
Clock skew causes confusing intermittent failures. If your auth server and API server clocks are out of sync by more than a few seconds, tokens might be rejected as "not yet valid" or "already expired" even when they look correct. Use NTP to synchronize clocks across all servers. Most JWT libraries allow a few seconds of clock skew tolerance (leeway parameter). Set this to 30-60 seconds to handle normal clock drift. But do not use large leeway values to paper over clock sync problems. Fix the underlying clock sync.
Issuer and audience mismatches happen when tokens are used across environments. A staging token might have iss: "staging.example.com" but production expects iss: "api.example.com". Always validate these fields match your environment. This prevents tokens from one environment being used in another. It also prevents accidentally accepting tokens from external systems. Validate issuer and audience in every verification. Treat mismatches as hard failures, not warnings.
Token refresh logic often has race conditions. If multiple requests try to refresh simultaneously, you can end up with conflicting tokens or cascading auth failures. Implement refresh locking or use refresh tokens that are single-use. When a client detects an expired access token, it should acquire a lock, check if another request already refreshed, and only call the refresh endpoint if needed. After refreshing, invalidate the old refresh token to prevent reuse. This prevents parallel requests from creating token chaos.
Key rotation breaks old tokens. When signing keys change, existing tokens become invalid. Plan rotation carefully with grace periods where both old and new keys are accepted. Maintain a key set with multiple active keys identified by kid (key ID) in JWT header. When rotating, add new key to the set, start signing with new key, but continue accepting old key for days or weeks. This gives issued tokens time to expire naturally. Only remove old key after all tokens signed with it have expired.
Scope and permission claims must be validated. Just because a token is valid does not mean it grants access to all resources. Check specific permissions before allowing operations. Access tokens might have read but not write scope. They might grant access to certain resources but not others. Parse scope claims and validate them against required permissions for each endpoint. Coarse-grained access control at the API gateway combined with fine-grained checks in services provides defense in depth.
Token storage location matters. Storing JWTs in localStorage makes them vulnerable to XSS attacks. HttpOnly cookies are safer but require CSRF protection. LocalStorage is accessible to any JavaScript on your domain, including third-party scripts and injected XSS payloads. HttpOnly cookies cannot be read by JavaScript, which protects them from XSS. But cookies are sent automatically on every request, which creates CSRF risk. Use SameSite=Strict or SameSite=Lax cookie attributes and implement CSRF tokens for state-changing operations.
Token lifetime should match use case. Short-lived access tokens (minutes to hours) are safer. Long-lived refresh tokens require secure storage and rotation. Access tokens are sent on every API request, which increases exposure. Keep them short-lived (15-60 minutes). Refresh tokens are used rarely and can be longer-lived (days to months). Store refresh tokens securely and rotate them on each use. This limits damage if tokens are compromised.
Multi-tenant systems must validate tenant claims. A valid token for one tenant should not grant access to another tenant's data. Always check tenant IDs. Include tenant ID in token claims. Validate it matches the tenant context of the current request. This prevents horizontal privilege escalation where a user in tenant A accesses tenant B data. Tenant validation should happen early in request processing, ideally at API gateway level.
JTI claim (JWT ID) enables token revocation. Without it, revoking a token before expiration is difficult. Include unique JTI in every token and maintain a revocation list. When a user logs out or you detect compromise, add the JTI to revocation list. Verify every token against revocation list during validation. This adds latency and state, so use short token lifetimes and refresh to minimize need for explicit revocation.
NBF claim (not before) prevents tokens from being used before they are valid. This matters when issuing tokens for future use. For example, pre-generating tokens for scheduled jobs. Set NBF to future timestamp and verify it during validation. Like EXP, NBF requires clock synchronization across systems.
Token binding ties tokens to specific clients or devices. Include client fingerprint or device ID in claims. Validate it matches the current request context. This prevents stolen tokens from being used on different devices. But be careful with fingerprinting—it can break legitimate use cases like users switching devices.
Token size affects performance. Each claim adds bytes. Large tokens increase network overhead and can hit header size limits. Keep claims minimal. Use short names. Reference data by ID instead of embedding full objects. If tokens exceed 4KB, you might hit HTTP header limits or cookie size restrictions. Consider using reference tokens (opaque tokens looked up server-side) for very large payloads.
Token replay attacks can be prevented with nonce or timestamp validation. Include request-specific nonce in token and verify it is not reused. Or validate timestamp claims to ensure tokens are not stale. This prevents attackers from capturing valid tokens and replaying them.
Never paste production tokens into public debugging tools or browser extensions. Even if the tool claims to be client-side only, you are exposing sensitive credentials to unnecessary risk. Use sanitized test tokens for examples. JWT debugging tools like jwt.io are convenient but should never see real tokens. Generate test tokens with fake user IDs and claims for debugging. If you must debug a production token, redact sensitive claims first. Better yet, reproduce the issue in a development environment with safe tokens.
Redact tokens from logs by default. If you log requests for debugging, mask the Authorization header or replace it with a placeholder. Accidental token leakage in logs is a common security incident root cause. Implement logging middleware that automatically redacts Bearer tokens. Show only last 4 characters for debugging: "Authorization: Bearer ...xY2q". This confirms the header is present without exposing the full token. Store logs securely and limit access. Assume logs will eventually leak—do not put secrets in them.
When reviewing auth failures, look for patterns before blaming configuration. If only certain users fail, check their token claims for outliers. If failures are time-correlated, suspect clock skew or token expiry policy issues. Use structured logging with fields like user_id, token_iss, token_exp, and failure_reason. This makes pattern analysis easier. Aggregate failures by reason to identify systemic issues versus user-specific problems.
Document your auth debugging runbook with exact commands and tools. This reduces ad hoc troubleshooting and ensures all team members use safe practices. Include links to token inspection tools that are known-safe. Document how to: decode tokens safely, verify signatures locally, check expiration, validate issuer and audience, test token refresh flows. A good runbook turns auth debugging from black magic into repeatable process.
Token rotation during active sessions needs careful handling. Users should not be logged out unexpectedly. Implement token refresh that happens transparently in background. Detect approaching expiration and proactively refresh tokens before they expire. Use silent refresh in hidden iframes or background fetch requests. Handle refresh failures gracefully by logging user out with a clear message, not just failing API requests cryptically.
Monitoring failed auth attempts helps identify attacks. Sudden spikes in 401 responses might indicate credential stuffing or token theft attempts. Track auth failure rate by IP, user agent, and endpoint. Alert on anomalies. Failed auth attempts could be legitimate (expired tokens, misconfigured clients) or malicious (brute force, stolen tokens). Distinguish between them using context clues like failure reason and request patterns.
Rate limiting on token endpoints prevents brute force attacks. Limit refresh token usage and failed login attempts per IP or account. Token endpoints are attractive targets for attackers. Limit refresh endpoint to prevent rapid token generation. Limit login endpoint to slow down credential guessing. Use progressive delays or temporary lockouts after repeated failures. But be careful not to enable denial-of-service where attackers lock out legitimate users.
Security headers like HSTS, X-Content-Type-Options, and X-Frame-Options complement JWT security. Defense in depth matters. HSTS enforces HTTPS and prevents protocol downgrade attacks. X-Content-Type-Options prevents MIME sniffing attacks. X-Frame-Options prevents clickjacking. Content-Security-Policy restricts what scripts can run. These headers do not directly protect tokens but reduce attack surface for exploits that might steal tokens.
Token encryption can add another security layer. While JWT signatures prevent tampering, they do not hide content. Anyone can decode and read claims. JWE (JSON Web Encryption) encrypts the entire token so only intended recipients can read it. This protects sensitive claims from exposure in transit or logs. But encryption adds complexity and performance cost. Use it when tokens contain genuinely sensitive data beyond user ID.
Certificate pinning for token endpoints prevents man-in-the-middle attacks. Mobile apps should pin the expected certificate or public key for auth servers. This prevents rogue certificates from being trusted. But pinning requires careful management—you must update apps when certificates rotate.
Device attestation can verify that tokens are being used by legitimate apps. Include device attestation data in token requests and verify it server-side. This prevents tokens from being extracted and used in attacker-controlled environments. Technologies like SafetyNet (Android) or DeviceCheck (iOS) provide attestation.
Anomaly detection on token usage patterns catches compromises. If a token is suddenly used from a new geographic location or device, flag it for additional verification. Machine learning models can detect abnormal patterns that indicate stolen tokens.
Secrets management for signing keys is critical. Never hardcode secrets in source code. Use environment variables, secret management services (AWS Secrets Manager, HashiCorp Vault), or key management systems. Rotate secrets regularly. Limit access to secrets to only services that need them.
Audit logs for auth events help forensics after incidents. Log every token issue, refresh, and revocation with full context. Include IP address, user agent, user ID, success/failure, and reason. Retain logs long enough to investigate incidents that are discovered late. Immutable audit logs prevent tampering by attackers trying to hide their tracks.
Read more articles on the FlexKit blog