Most JWT bugs in TypeScript are not in the token — they are in the verification configuration. A missing algorithm constraint, an unchecked issuer claim, or a short-lived secret can silently undermine authentication in a way that only surfaces in production. This guide walks through every dimension of JWT configuration correctness in TypeScript: algorithms, claim validation, secret hygiene, expiry handling, and the browser-based tools that make debugging fast.
What JWT configuration validation covers
A JSON Web Token (JWT) is a compact, URL-safe string made of three Base64URL-encoded parts separated by dots: a header declaring the algorithm and token type, a payload carrying claims, and a signature binding them together. Validating a JWT means confirming that all three parts are intact and that the claims meet your application's requirements — not just that the signature is mathematically correct.
The two layers of JWT validation
Cryptographic validation confirms the signature: the server verifies that the token was signed with the expected key and has not been tampered with. Configuration validation goes further: it checks that the token was issued by the right authority, is intended for this specific service, has not expired, and carries the expected custom claims. Most JWT security vulnerabilities come from incomplete configuration validation, not from broken cryptography.
- Algorithm (`alg`) — must match your server configuration exactly; never infer from the token header
- Expiry (`exp`) — token must not be past its expiry timestamp, accounting for clock tolerance
- Not-before (`nbf`) — token must not be used before its earliest valid time
- Issuer (`iss`) — token must originate from your trusted auth service
- Audience (`aud`) — token must be intended for this specific API or service
- Custom claims — role, scope, tenant ID, or any application-specific fields your logic depends on
Note
Algorithm and key configuration checks
The algorithm configuration is the single most security-critical setting in JWT verification. Getting it wrong enables a class of attacks that bypass authentication entirely. TypeScript JWT libraries give you the tools to enforce it correctly — but only if you use them explicitly.
HS256 vs RS256 — choosing the right algorithm
| Property | HS256 (symmetric) | RS256 (asymmetric) |
|---|---|---|
| Key type | Shared secret (same key for sign + verify) | RSA key pair (private to sign, public to verify) |
| Key distribution | Every verifier holds the secret | Only the issuer holds the private key |
| Multi-service safety | ✗ Risky — all verifiers can forge tokens | ✓ Verifiers hold only the public key |
| OIDC / JWKS support | ✗ Not applicable | ✓ Public keys served via JWKS endpoint |
| Performance | ✓ Fast (HMAC) | ✗ Slower (RSA math) |
| Best for | Internal tools, single-service APIs | Production APIs, distributed systems, OIDC |
The algorithm confusion attack — and how to prevent it
Algorithm confusion happens when a server reads the `alg` field from the JWT header to decide how to verify the token, rather than enforcing the algorithm from its own configuration. An attacker modifies the header to change `RS256` to `HS256`, then signs the token with the server's public key used as the HMAC secret. A misconfigured server accepts it as valid. The fix is a single line of code — but it must be present.
// ✗ Vulnerable — algorithm inferred from token header
jwt.verify(token, publicKey);
// ✓ Correct — algorithm enforced from server configuration
jwt.verify(token, publicKey, { algorithms: ['RS256'] });Warning
Key strength validation for HS256
When using HS256, the secret must be at least 256 bits (32 bytes) to match the output size of SHA-256. Short secrets — less than 32 characters, dictionary words, or static strings like `"secret"` or `"development"` — are trivially brute-forced with tools like `hashcat` or `jwt_tool`. Generate secrets with a cryptographically secure random source: `crypto.randomBytes(32).toString('hex')` in Node.js produces a 64-character hex string that satisfies the minimum entropy requirement.
Validating standard JWT claims in TypeScript
The JWT specification defines a set of standard registered claims that every implementation should understand. The `jsonwebtoken` library validates several of these automatically when you pass the right options — but the key word is "when you pass them." Without explicit configuration, most claim checks are silently skipped.
The exp, nbf, and iat claims
The exp (expiry) claim is a Unix timestamp after which the token is no longer valid. The jsonwebtoken library checks exp by default during jwt.verify(). However, clock skew between the token issuer and the verifier can cause valid tokens to be rejected — a common source of "token expired" errors in distributed systems where server clocks drift by seconds. Pass clockTolerance to allow a small window: setting clockTolerance to 30 accepts tokens up to 30 seconds past their exp value.
interface JwtPayload {
sub: string;
iss: string;
aud: string;
exp: number;
iat: number;
role: 'admin' | 'user';
}
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 30, // seconds of clock skew to tolerate
}) as JwtPayload;The iss and aud claims
The `iss` (issuer) claim identifies the token's origin. The `aud` (audience) claim identifies the intended recipient. Both are optional in the JWT spec but critical in practice. Without `iss` validation, any service that can produce valid tokens with your signing key can authenticate into your API. Without `aud` validation, a token issued for your mobile app can be replayed against your admin API. Pass both as options to `jwt.verify()` so that the library enforces them as hard requirements rather than informational fields.
Tip
Implementing JWT validation in TypeScript
A complete JWT verification function in TypeScript handles cryptographic validation, claim validation, and error classification in one place. Here is how to structure it — with the four steps that match the HowTo schema.
Verify the algorithm matches your key type
Before writing any verification code, confirm your algorithm and key combination. RS256 requires an RSA private key for signing and the corresponding public key for verification. HS256 requires the same shared secret on both sides. Mixing these up causes runtime exceptions that are difficult to diagnose. Store your public key or shared secret in environment variables — never hardcode them in source files.
Validate the exp and nbf claims explicitly
Always set `clockTolerance` to handle minor clock drift between services. A value of 30 seconds is a reasonable default for most distributed systems. When debugging `TokenExpiredError` in production, log `payload.exp * 1000` and `Date.now()` together — this shows exactly how many milliseconds past expiry the token was, which distinguishes real expiry from a clock synchronisation problem between your auth service and API server.
Check the iss and aud claims against expected values
Pass `issuer` and `audience` options to `jwt.verify()` so the library rejects tokens with mismatched values before your application code runs. If your system has multiple valid audiences (e.g. both `api.example.com` and `admin.example.com`), pass an array: `audience: ['api.example.com', 'admin.example.com']`. The library accepts the token if the `aud` claim matches any entry in the array.
Test your configuration with the JWT Decoder
Before running your TypeScript test suite, paste a sample token from your development or staging environment into the JWT Decoder and Validator. Visually confirm that every claim — `alg`, `exp`, `iss`, `aud`, and your custom claims — matches your `jwt.verify()` options. This one-minute check catches mismatches between what the token contains and what your code expects, before spending time debugging in a test environment.
JWT Decoder and Validator
Decode JWT tokens and inspect all claims, header fields, and common security issues instantly in your browser — no signup, no server upload.
Common JWT configuration mistakes
These are the configuration errors that appear most frequently in TypeScript JWT implementations. Each one is silent at startup and only manifests as an authentication failure or a security incident in production.
Using `jwt.decode()` instead of `jwt.verify()`
The `jwt.decode()` function extracts the payload without verifying the signature. It is useful for inspecting a token you already trust — such as extracting a user ID from a token that was already validated by middleware. It is not a substitute for `jwt.verify()`. Code that uses `jwt.decode()` to obtain claims and then makes authorization decisions based on those claims is accepting unverified tokens. This is a complete authentication bypass.
Ignoring the error type in catch blocks
The `jsonwebtoken` library throws three distinct error types: `JsonWebTokenError` (malformed token or invalid signature), `TokenExpiredError` (past the `exp` claim), and `NotBeforeError` (before the `nbf` claim). Catching all errors as a generic `Error` and returning `401 Unauthorized` for all cases loses diagnostic information. Handle each type separately and return specific messages — `token expired` vs `invalid token` — so clients and monitoring systems can distinguish configuration problems from genuine attack attempts.
Not rotating secrets or key pairs
Long-lived signing secrets accumulate risk over time. A secret that has never been rotated means that every token ever issued with it remains valid if the secret is compromised. Implement a key ID (`kid`) field in your JWT header so that the verifier can look up the correct public key from a JWKS endpoint. This pattern allows key rotation without invalidating tokens signed by the previous key — each token carries a reference to the specific key that signed it. The JWK Inspector helps validate JWKS endpoint output and detect private key material that should not be publicly exposed.
Warning
Accepting the `none` algorithm
The `none` algorithm produces an unsigned JWT — any payload with a valid structure passes verification. Early JWT library versions accepted `none` by default. Modern libraries reject it, but only when you explicitly specify allowed algorithms in the verify options. Always include `algorithms: ['RS256']` (or your specific algorithm) to make `none` rejection explicit and resilient to library version changes.
Debugging JWT issues with browser tools
Writing a test to reproduce a JWT error is often slower than inspecting the token directly. Browser-based tools let you examine a token's claims, expiry state, and key structure in seconds — without a local environment, without running code, and without uploading sensitive data to a third-party service.
Decoding and inspecting claims
The JWT Decoder and Validator decodes the header and payload of any JWT string and presents all claims in a structured, readable format. It checks for common security issues — missing `exp`, weak algorithm, missing `aud` — and flags them with clear diagnostics. Paste any token from your development, staging, or production environment and confirm in ten seconds whether the claims match what your `jwt.verify()` call expects.
Diagnosing expiry issues
The JWT Expiry Countdown Calculator reads the `exp` claim from any token and shows the exact remaining lifetime or time since expiry in both UTC and local time. When a user reports "token expired" but your logs show the token should still be valid, paste it into the calculator. The millisecond-level timestamp comparison reveals whether the issue is genuine expiry, clock drift between services, or an `exp` value stored in milliseconds instead of seconds — a surprisingly common off-by-1000 bug.
Inspecting JWKS key sets
When validating tokens from an OIDC provider or any service that publishes a JWKS endpoint, the JWK Inspector parses and validates the key set structure. It checks that each key has the required fields (`kty`, `use`, `alg`, `kid`), validates the key type and curve for EC keys, and flags any private key material that should not be publicly exposed. Paste the JSON from your JWKS endpoint directly into the tool or provide the URL for a fetch.
JWT Expiry Countdown Calculator
Calculate exact JWT expiry countdown from the exp claim — see remaining time or time since expiry in UTC and local time, no code required.
JWT validation best practices
A correctly configured JWT verification function is necessary but not sufficient for secure authentication. These practices complete the picture for production TypeScript services.
Use a typed payload interface
Define a TypeScript interface for your JWT payload and cast the verify result to it. This gives you compile-time safety on claim names and value types — a misspelled claim name (`userId` vs `user_id`) becomes a TypeScript error rather than a silent runtime `undefined`. Keep the interface in a shared types module so it is consistent across all services that verify the same token format.
Short token lifetimes with refresh tokens
Access tokens should have short lifetimes — 15 minutes to 1 hour for most APIs. Long-lived access tokens (days, weeks) increase the window during which a compromised token can be used. Use a separate refresh token flow with long expiry for session persistence. The refresh token rotates on each use, and revocation lists are only needed for refresh tokens — not access tokens — when lifetimes are kept short.
Centralise verification logic
Write JWT verification in one place — a middleware function or a shared utility — and use it everywhere. Duplicated verification logic invites configuration drift: one endpoint checks `aud`, another forgets to, and the inconsistency is exploited before anyone notices. Express middleware, NestJS guards, and Next.js route handlers all have clean patterns for centralising auth checks. Put your `algorithms`, `issuer`, and `audience` options in a single configuration object imported across the codebase.
Configure verification once, enforce it everywhere. The only JWT option that should differ between endpoints is the expected audience.
Tip
Key takeaways
- Always pass `algorithms: ['RS256']` (or your specific algorithm) explicitly to `jwt.verify()` — never let the library infer it from the token header.
- Validate `iss` and `aud` claims in every verification call; omitting them allows tokens from other services to authenticate against your API.
- Use `clockTolerance` to handle clock drift between distributed services, and log `exp` timestamps alongside `Date.now()` when debugging expiry errors.
- Never use `jwt.decode()` for authorization decisions — it skips signature verification entirely and accepts any token payload.
- The JWT Decoder and Validator lets you inspect all claims and flag configuration issues in seconds, without writing or running code.
- RS256 is preferred over HS256 for production APIs — it eliminates the shared-secret distribution risk and supports JWKS-based key rotation.
- Keep access token lifetimes short (15–60 minutes) and centralise all verification logic in a single middleware or utility to prevent configuration drift.