Skip to content
Quasar Tools Logo

How to Validate JWT Configuration in TypeScript

How to validate JWT configuration correctness in a TypeScript project — algorithms, expiry, claims, secret strength, and browser-based debugging tools.

DH
Tutorials & How-Tos12 min read2,750 words

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.

3JWT header partsheader · payload · signature
RS256Recommended algorithmAsymmetric, production-safe
0 KBServer uploadsBrowser-based JWT debugging

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

JWT structure validation (checking that the token is a valid three-part Base64URL string) is a prerequisite to all other validation. The [JWT Decoder and Validator](/tools/data/validators/jwt-decoder-and-validator) handles this instantly in your browser — useful for confirming a token is well-formed before writing verification code.

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

PropertyHS256 (symmetric)RS256 (asymmetric)
Key typeShared secret (same key for sign + verify)RSA key pair (private to sign, public to verify)
Key distributionEvery verifier holds the secretOnly 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 forInternal tools, single-service APIsProduction 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.

typescript
// ✗ Vulnerable — algorithm inferred from token header
jwt.verify(token, publicKey);

// ✓ Correct — algorithm enforced from server configuration
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Warning

Never omit the `algorithms` option in `jwt.verify()`. Even if your current library version defaults to rejecting the `none` algorithm, explicitly listing the allowed algorithms in your code makes the intent clear, survives library upgrades, and eliminates the algorithm confusion class of vulnerability entirely.

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.

typescript
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

Check the `iss` and `aud` values of a token you received by pasting it into the [JWT Decoder and Validator](/tools/data/validators/jwt-decoder-and-validator). The decoded payload shows every claim in a readable format, making it easy to confirm that your issuer and audience strings match what the library is configured to expect.

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.

1

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.

2

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.

3

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.

4

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.

Open tool

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

Never commit JWT secrets or private keys to version control. Use environment variables for all key material, load them at runtime, and confirm they are set before accepting any requests. An application that starts with an undefined or empty secret will silently accept tokens signed with an empty string.

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.

Open tool

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.

Secure JWT implementation principle

Tip

When adopting a new auth provider or upgrading your JWT library, use the [JWT Decoder and Validator](/tools/data/validators/jwt-decoder-and-validator) to inspect tokens from the new source before updating your verification configuration. This confirms the exact `alg`, `iss`, and `aud` values in the new tokens so your code changes match reality — not assumptions.

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.

Frequently Asked Questions

Use the `jsonwebtoken` library (or `jose` for a modern alternative) and call `jwt.verify(token, secret, { algorithms: ['RS256'], issuer: 'your-issuer', audience: 'your-audience' })`. Always specify the `algorithms` array explicitly — never allow the library to infer it from the token header, as this enables algorithm confusion attacks. Wrap the call in a try/catch and handle `JsonWebTokenError`, `TokenExpiredError`, and `NotBeforeError` separately so you can return informative error responses to API clients.

An algorithm confusion attack occurs when a server accepts the `alg` field from the JWT header to determine how to verify the signature, rather than enforcing the algorithm from its own configuration. An attacker can change `alg` from `RS256` to `HS256` in the header, sign the token with the server's public key as the HMAC secret, and the server will incorrectly validate it as legitimate. The fix: always pass `algorithms: ['RS256']` (or your specific algorithm) explicitly in the verify options.

Call `jwt.verify()` — it throws a `TokenExpiredError` if the `exp` claim is in the past. To inspect expiry without throwing, decode the payload with `jwt.decode(token)` and compare `payload.exp * 1000` to `Date.now()`. For a visual expiry check without writing code, paste your token into the Quasar Tools JWT Expiry Countdown Calculator, which shows the exact remaining time or time since expiry in both UTC and local time.

Yes — both are critical. The `iss` (issuer) claim identifies who created the token. Without verifying it, your application will accept tokens issued by any service, including attackers. The `aud` (audience) claim identifies the intended recipient. Without verifying it, a token issued for one of your services can be replayed against another. Pass both as options: `{ issuer: 'https://auth.example.com', audience: 'api.example.com' }`.

HS256 (HMAC-SHA256) uses a single shared secret for both signing and verification. It is simpler to implement but requires every service that verifies tokens to hold the same secret — a security risk in distributed systems. RS256 (RSA-SHA256) uses a private key to sign and a public key to verify. Only the issuing service holds the private key; all consuming services use the public key. RS256 is the recommended algorithm for production APIs where tokens are verified by multiple services or third parties.

After `jwt.verify()` succeeds, cast the result to a typed interface and assert your custom claim values. For example: `const payload = jwt.verify(token, secret) as MyPayload; if (payload.role !== 'admin') throw new Error('Insufficient role')`. Using a TypeScript interface for your JWT payload type gives you compile-time safety on claim names and value types. Validate any claim whose absence or wrong value would represent a security failure — not just standard claims.

The `jose` library is a modern, standards-compliant implementation of JWT, JWS, JWE, JWK, and JWKS that works in Node.js, browsers, Deno, and edge runtimes like Cloudflare Workers. Use `jose` when you need JWKS endpoint support for OIDC, when building for edge or serverless environments, or when you need JWE (encrypted JWT) support. Use `jsonwebtoken` for simple HS256 or RS256 signing and verification in traditional Node.js backends where a shared-secret or static key is sufficient.

Paste the JWT into the Quasar Tools JWT Decoder and Validator at quasartools.com/tools/data/validators/jwt-decoder-and-validator. It decodes the header and payload, shows all claims in a readable format, checks for common configuration issues, and flags security problems — all in your browser with no server upload. For expiry checks, the JWT Expiry Countdown Calculator shows exactly how much time remains or how long ago the token expired.

ShareXLinkedIn

Related articles