Why JWT verification deserves careful handling

A JWT is only trustworthy after it has been cryptographically verified and its claims have been checked against your policy. A decoded token is not a validated token.

Common mistakes include:

  • Accepting tokens with alg: none or an unexpected algorithm
  • Using the wrong key type for verification
  • Failing to validate exp, nbf, or iss
  • Trusting claims before signature verification
  • Reusing tokens across services without audience checks

In Rust, the goal is to make invalid states hard to represent and hard to use. That means separating parsing from verification, and verification from authorization decisions.

Choose a library that enforces verification first

For most applications, the jsonwebtoken crate is a practical choice. It supports common signing algorithms and provides typed claims, which helps you keep token handling structured.

A secure JWT flow should look like this:

  1. Receive the token as an opaque string
  2. Verify the signature with an expected algorithm and key
  3. Validate standard claims
  4. Apply application-specific authorization rules
  5. Only then use the claims

Avoid manually base64-decoding the token and reading the payload before verification. The payload is attacker-controlled until the signature is checked.

Define claims with explicit types

Typed claims help you express what your application expects. Keep the structure minimal and avoid storing sensitive data in the token.

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    iss: String,
    aud: String,
    exp: usize,
    nbf: Option<usize>,
    scope: Option<String>,
}

A few practical rules:

  • Use sub for the subject or user identifier
  • Require iss if tokens come from a specific issuer
  • Require aud if tokens are meant for a specific service
  • Keep scope or roles as coarse-grained authorization hints, not final decisions
  • Avoid embedding secrets, passwords, or long-lived permissions in the token

JWTs are signed, not encrypted, unless you use JWE. Anyone who can obtain the token can usually read its contents.

Verify the signature with an expected algorithm

The most important defense is to reject tokens unless they are signed with the algorithm you expect. Never let the token choose the verification method.

Here is a secure verification example using HMAC SHA-256:

use jsonwebtoken::{
    decode, Algorithm, DecodingKey, Validation,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    iss: String,
    aud: String,
    exp: usize,
}

fn verify_token(token: &str, secret: &[u8]) -> Result<Claims, jsonwebtoken::errors::Error> {
    let mut validation = Validation::new(Algorithm::HS256);
    validation.set_issuer(&["auth.example.com"]);
    validation.set_audience(&["api.example.com"]);

    let key = DecodingKey::from_secret(secret);
    let token_data = decode::<Claims>(token, &key, &validation)?;
    Ok(token_data.claims)
}

This example does three important things:

  • Locks verification to HS256
  • Requires a specific issuer
  • Requires a specific audience

If your system uses asymmetric signing, such as RS256 or ES256, use the corresponding public key for verification and keep the private key only on the issuer side.

Understand the difference between authentication and authorization

A verified JWT tells you who issued the token and whether the signature is valid. It does not automatically authorize every action.

For example:

  • Authentication: “This token belongs to user alice.”
  • Authorization: “alice may read invoices, but not delete them.”

A common mistake is to treat a role claim as a complete authorization system. Instead, use token claims as input to your policy engine or access-control layer.

Recommended pattern

ConcernJWT claimDecision point
IdentitysubAfter verification
Token originissAfter verification
Intended serviceaudAfter verification
Token freshnessexp, nbfDuring verification
Permissionsscope, rolesApplication policy layer

This separation keeps token validation and business authorization from becoming tangled.

Validate time-based claims carefully

Expiration and not-before checks are essential. Tokens should be short-lived, especially for browser clients and mobile apps.

The jsonwebtoken crate validates exp by default when configured with Validation. You should still think carefully about clock skew and token lifetime.

Best practices for time claims

  • Keep access tokens short-lived, often 5–15 minutes
  • Use refresh tokens separately, with stricter storage and rotation rules
  • Allow a small clock skew if your infrastructure is distributed
  • Reject tokens missing required time claims when your policy depends on them

If your system has multiple services with slightly different clocks, configure a small leeway rather than disabling time checks.

Avoid trusting unverified headers

JWT headers can include fields such as kid, typ, and alg. These fields are useful, but they are also attacker-controlled until verification succeeds.

The kid field is especially sensitive if you use it to select a key from a database or key set. A naive implementation can become vulnerable to key confusion or even injection-like behavior if the lookup logic is unsafe.

Safer key selection strategy

  • Maintain a fixed allowlist of key IDs
  • Map kid values to known public keys
  • Reject unknown or malformed kid values
  • Never use kid to build file paths or database queries directly

If you rotate keys, publish a controlled JWKS and cache it safely. Do not fetch arbitrary keys from token-provided URLs.

Handle key material securely

The security of JWT verification depends on the integrity of your keys.

For HMAC-based tokens:

  • Use a long, random secret
  • Store it in a secret manager or protected environment variable
  • Rotate it periodically
  • Avoid embedding it in source code or container images

For asymmetric tokens:

  • Keep private keys offline or in a secure signing service
  • Distribute only public keys to verifiers
  • Pin the expected key set or issuer metadata

A practical operational rule is to separate signing and verification responsibilities. Verifiers should not have access to signing keys.

Reject malformed tokens early

A secure verifier should fail closed on malformed input. That includes:

  • Tokens with the wrong number of segments
  • Invalid base64url encoding
  • Unsupported algorithms
  • Missing required claims
  • Claims with unexpected types

Do not try to “fix up” malformed tokens. Lenient parsing can create ambiguity and increase the attack surface.

In Rust, prefer returning errors rather than panicking. JWT verification is part of your trust boundary, so every failure should be explicit and auditable.

Build a safe verification pipeline

A robust token-processing pipeline usually follows this order:

  1. Read the token as a string
  2. Check basic format constraints
  3. Verify signature with a fixed algorithm
  4. Validate standard claims
  5. Enforce service-specific policy
  6. Convert claims into an internal authorization context

This approach prevents untrusted data from flowing into business logic too early.

Example: turning claims into an application context

struct AuthContext {
    user_id: String,
    scopes: Vec<String>,
}

fn to_auth_context(claims: Claims) -> Result<AuthContext, &'static str> {
    let scopes = claims
        .scope
        .unwrap_or_default()
        .split_whitespace()
        .map(str::to_string)
        .collect();

    Ok(AuthContext {
        user_id: claims.sub,
        scopes,
    })
}

This conversion should happen only after verification. If the token is invalid, no context should be created.

Common JWT mistakes and safer alternatives

MistakeRiskSafer alternative
Accepting any algorithmAlgorithm confusionHardcode the expected algorithm
Reading claims before verificationTrusting attacker-controlled dataVerify first, parse second
Skipping aud validationToken reuse across servicesRequire a specific audience
Using long-lived access tokensIncreased replay windowShort-lived access tokens with refresh flow
Storing sensitive data in claimsExposure if token is leakedStore only minimal identifiers
Using kid without allowlistingUnsafe key selectionMap known key IDs to known keys

These issues are common because JWTs are easy to decode and inspect. Security comes from disciplined verification, not from the token format itself.

Testing your JWT handling

Security-sensitive code should be tested with both valid and invalid inputs. Include cases such as:

  • Expired token
  • Token with wrong issuer
  • Token with wrong audience
  • Token signed with the wrong key
  • Token using an unsupported algorithm
  • Token with missing required claims
  • Token with malformed base64 segments

Property-based tests can also help ensure that malformed input never reaches authorization logic. In Rust, you can combine unit tests with fuzzing to exercise edge cases in token parsing and validation.

Operational guidance for production systems

JWT security is not only a code problem. It is also an operational one.

Recommended practices

  • Keep access token lifetimes short
  • Rotate signing keys and publish key changes carefully
  • Log verification failures without logging the token itself
  • Use HTTPS everywhere tokens are transmitted
  • Prefer secure storage for refresh tokens
  • Revoke tokens through short lifetimes or server-side session state when necessary

If you need immediate revocation, JWTs alone are often not enough. In that case, consider a hybrid design with short-lived JWTs plus server-side revocation checks.

When not to use JWTs

JWTs are not always the best solution. If your application needs:

  • Immediate revocation
  • Centralized session control
  • Very small attack surface
  • Simple opaque credentials

then opaque session tokens or server-side sessions may be easier to secure. JWTs are best when stateless verification, distributed services, or third-party identity integration are important.

Conclusion

JWTs are useful, but they are easy to misuse. In Rust, the safest approach is to treat every token as untrusted until it has been cryptographically verified and its claims have been validated against explicit policy.

Focus on a narrow set of claims, lock verification to a known algorithm, validate issuer and audience, and keep authorization separate from authentication. With those habits, JWT handling becomes much safer and much easier to reason about.

Learn more with useful resources