How to Fix JWT Token Vulnerabilities in Laravel

Weak JWT implementation in your Laravel API can lead to authentication bypass and token forgery. Learn how to secure your JWT setup.

High severity Application Security Updated 2026-03-01 Markdown

JSON Web Tokens were introduced as a solution to a real problem: stateless authentication across multiple servers or services without shared session storage. The token encodes claims (user ID, expiration, roles) in a base64-encoded JSON payload, signs it with a secret or private key, and sends it to the client. Subsequent requests include the token, allowing any server with the verification key to authenticate the request without a database lookup. For microservices and mobile applications, this is genuinely useful.

However, the JWT specification was designed to be extremely flexible, and that flexibility created serious security problems. The algorithm confusion attack (also called the "alg:none" attack) exploited libraries that accepted the algorithm from the token header itself — an attacker could set "alg": "none", strip the signature, and some libraries would accept the unsigned token as valid. The HS256 to RS256 confusion attack exploited libraries that allowed symmetric verification of asymmetrically signed tokens. These vulnerabilities affected JWT libraries across every programming language and were exploited widely before patches were released. Many Laravel applications using tymon/jwt-auth before version 1.0.2 were affected.

Beyond library vulnerabilities, JWTs have inherent architectural challenges that make them harder to secure than session-based authentication. They cannot be invalidated server-side without maintaining a token blacklist (which partially negates the stateless benefit). Storing JWTs in localStorage makes them vulnerable to any XSS attack. Storing them in cookies reintroduces CSRF risk. For most Laravel SPA authentication scenarios, Sanctum's cookie-based authentication avoids all of these tradeoffs and is now the Laravel-recommended default.

The Problem

Weak JWT (JSON Web Token) implementations allow attackers to forge authentication tokens, bypass authorization, and impersonate users. Common vulnerabilities include using weak signing algorithms (none or HS256 with weak secrets), not validating token expiration, accepting tokens with modified algorithms, and storing sensitive data in the JWT payload. These issues are especially prevalent in Laravel API applications using packages like tymon/jwt-auth.

How to Fix

  1. 1

    Use a strong signing secret

    Generate a strong JWT secret using the package command:

    php artisan jwt:secret

    This generates a random 64-character secret and adds it to your .env:

    JWT_SECRET=your-generated-64-character-secret
    Never use short, guessable secrets. The secret should be at least 256 bits (32 characters) for HS256. Consider using RS256 (asymmetric) for additional security:
    # In config/jwt.php
    'algo' => 'RS256',
    'keys' => [
        'public' => env('JWT_PUBLIC_KEY'),
        'private' => env('JWT_PRIVATE_KEY'),
    ],
  2. 2

    Set appropriate token expiration

    Configure short-lived access tokens with refresh token rotation. In config/jwt.php:

    'ttl' => 15,
    'refresh_ttl' => 20160,
    'blacklist_enabled' => true,

    Implement refresh token rotation in your controller:

    public function refresh()
    {
        try {
            $newToken = auth('api')->refresh();
            return response()->json(['token' => $newToken]);
        } catch (TokenExpiredException $e) {
            return response()->json(['error' => 'Token expired'], 401);
        }
    }
  3. 3

    Validate tokens strictly

    Ensure your JWT middleware validates all required claims. In your auth guard configuration (config/auth.php):

    'guards' => [
        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],

    Add custom claim validation in your auth controller:

    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);
    $token = auth('api')->claims([
            'iss' => config('app.url'),
            'aud' => 'api',
        ])->attempt($credentials);
    if (!$token) {
            return response()->json(['error' => 'Invalid credentials'], 401);
        }
    return response()->json(['token' => $token]);
    }
  4. 4

    Do not store sensitive data in JWT payload

    JWT payloads are base64-encoded, not encrypted. Anyone can read them. Never include:

    // BAD - sensitive data in payload
    $token = auth('api')->claims([
        'role' => 'admin',
        'permissions' => ['delete_users', 'edit_settings'],
        'email' => $user->email,
        'api_key' => $user->api_key,
    ])->fromUser($user);
    // GOOD - minimal payload, look up permissions server-side
    $token = auth('api')->claims([
        'sub' => $user->id,
        'iss' => config('app.url'),
    ])->fromUser($user);
    // In your controller, look up the full user from the token's sub claim
    $user = auth('api')->user();

How to Verify

Test your JWT implementation:

1. Decode a token at jwt.io and verify it does not contain sensitive data 2. Try using an expired token - it should return 401 3. Modify the payload of a valid token and send it - it should return 401 4. Send a token with alg:none in the header - it should be rejected

curl -H "Authorization: Bearer expired-or-tampered-token" https://yourdomain.com/api/user

This should return 401 Unauthorized.

Prevention

Consider using Laravel Sanctum for SPA authentication instead of JWT, as it uses session-based authentication which avoids many JWT pitfalls. If JWT is required, use well-maintained packages, rotate secrets regularly, and implement token blacklisting for logout. Monitor for unusual authentication patterns.

Frequently Asked Questions

Should I use JWT or Laravel Sanctum?

For SPAs communicating with their own backend, use Sanctum (session-based) as it avoids JWT complexity. Use JWT only when you need stateless authentication for third-party API consumers, mobile apps, or microservices that cannot share sessions. Sanctum also supports API tokens for these use cases.

What is the "none" algorithm attack?

Some JWT libraries accept tokens with the algorithm set to "none", which means no signature verification. An attacker can modify the token payload, set alg to none, remove the signature, and the server accepts it. Modern JWT packages reject alg:none by default, but always verify your configuration.

How do I handle JWT logout?

JWTs are stateless, so you cannot invalidate them server-side by default. Enable token blacklisting in your JWT package (blacklist_enabled = true), which stores invalidated token IDs in cache. On logout, add the token to the blacklist. The trade-off is that blacklisting requires server-side state, partially negating JWT's stateless benefit.

What data should and should not go in a JWT payload?

JWT payloads are base64-encoded, not encrypted — anyone with the token can read the payload by decoding it. Safe to include: user ID (sub), token expiration (exp), issuer (iss), and audience (aud). Never include: passwords, API keys, sensitive personal data, payment information, or role claims that could be used for privilege escalation if the signing key is ever compromised.

How do I implement secure refresh token rotation with tymon/jwt-auth?

Configure a short access token TTL (15 minutes) and longer refresh TTL (2 weeks) in config/jwt.php. On each refresh, call auth('api')->refresh() to issue a new access token and invalidate the old one (requires blacklist_enabled=true). Store refresh tokens in HttpOnly cookies rather than localStorage to protect against XSS theft. Detect refresh token reuse (a sign of token theft) and revoke the entire token family on detection.

Free security check

Is your Laravel app exposed right now?

34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 60 seconds.