JSON Web Tokens in PHP: When to Use Them, When to Avoid Them, and How to Validate Them

13 min read 2,459 words
JSON Web Tokens in PHP: When to Use Them, When to Avoid Them, and How to Validate Them featured image

What a JSON Web Token Actually Is

JSON Web Tokens, commonly abbreviated as JWT, are a standardised format for transmitting claims between two parties. In web development, they are most often used to authenticate users and transmit identity information between a frontend application and a backend API. A JWT is a string that contains three base64-encoded sections separated by dots: the header, the payload, and the signature.

The header typically specifies the algorithm used for signing, such as HS256 or RS256. The payload carries the actual data, often called claims, which can include a user identifier, roles, expiry time, and other metadata. The signature proves that the token was created by a trusted source and that the payload has not been altered.

PHP applications frequently use JWTs for API authentication, single sign-on systems, and any scenario where stateless authentication is preferable to maintaining server-side sessions. Understanding how they work under the hood helps you make informed decisions about when they are the right tool and when a different approach serves your application better.

How JWTs Work in a Typical PHP Application

When a user logs in to a PHP application that uses JWT authentication, the server validates the credentials and creates a token containing the relevant claims. This token is returned to the client, usually in the response body or as a cookie. On subsequent requests, the client sends the token along with each request, typically in the Authorization header as a Bearer token.

The server then decodes the token, verifies the signature using the secret key or public key depending on the algorithm, checks that the token has not expired, and extracts the claims to determine the user's identity and permissions. Because the server does not need to look up session data in a database or shared cache, this approach scales well in environments where multiple application servers handle requests.

Creating a JWT in PHP

The most practical way to create and validate JWTs in PHP is to use a well-maintained library. The firebase/php-jwt package is widely used and supports a range of algorithms. Install it via Composer if you have it available in your project.

composer require firebase/php-jwt

Once installed, creating a token involves defining the payload and signing it with a secret key. The payload should include the claims your application needs, along with standard claims such as the issued-at time and the expiration time.

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$payload = [
    'iss' => 'https://yourapplication.com',
    'sub' => '12345',
    'role' => 'admin',
    'iat' => time(),
    'exp' => time() + 3600
];

$secret = 'your-256-bit-secret-key';
$jwt = JWT::encode($payload, $secret, 'HS256');

echo $jwt;

The secret key should be a long, random string stored securely, ideally in an environment variable rather than hardcoded in your source files. The expiry time, labelled exp, should be set appropriately for your use case. Tokens issued for short-lived API access might expire after fifteen minutes, while tokens used in less sensitive contexts might last several hours.

Validating a JWT in PHP

Validation is where security mistakes are most commonly made. A JWT must be decoded carefully, with every check performed explicitly before trusting the token. Skipping any of these steps can expose your application to forged tokens.

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;

try {
    $jwt = $request->getHeaderLine('Authorization');
    $jwt = str_replace('Bearer ', '', $jwt);
    
    $decoded = JWT::decode($jwt, new Key($secret, 'HS256'));
    
    $userId = $decoded->sub;
    $role = $decoded->role;
    
} catch (ExpiredException $e) {
    http_response_code(401);
    echo json_encode(['error' => 'Token has expired']);
    exit;
} catch (SignatureInvalidException $e) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid token signature']);
    exit;
} catch (Exception $e) {
    http_response_code(401);
    echo json_encode(['error' => 'Token validation failed']);
    exit;
}

Always return a generic error message to the client when validation fails. Providing specific details about whether the signature was invalid or the token expired can aid attackers in understanding your authentication system. For a deeper look at common security vulnerabilities in web applications, the OWASP Top 10 provides a useful reference for the types of weaknesses to watch for during development.

Storing JWTs on the Client Side

Where you store the token on the client side affects the security of your authentication system. Storing a JWT in localStorage makes it accessible to JavaScript running on your page, which means it is vulnerable to cross-site scripting attacks. If an attacker can inject a script into your page, they can read the token and use it to impersonate the user.

Storing the token in an HTTP-only cookie mitigates this risk because JavaScript cannot access HTTP-only cookies. However, this approach introduces a different vulnerability: cross-site request forgery. Because the browser automatically includes cookies with every request to your domain, an attacker could trick a user into making a request that includes the authentication token.

CSRF protection requires additional measures, such as using a custom request header and validating the Origin or Referer header on the server. Many PHP applications use the symfony/security-bundle or a similar library to handle CSRF tokens alongside session-based or token-based authentication.

When JWTs Are the Right Choice

JWTs work well in several scenarios. If you are building a stateless API that serves multiple clients, including mobile apps and single-page applications, JWTs allow each server to authenticate requests without needing to share session storage. Microservices architectures benefit from this as well, because any service can verify a token using a shared secret or the issuer's public key.

When you need to pass user identity through multiple services in a distributed system, a JWT can carry the necessary claims across service boundaries. If you are implementing single sign-on across different domains, a JWT signed by a central authority can be verified by each participating service.

For PHP applications that serve as API backends to JavaScript frontends, JWTs are a practical choice because the frontend can store the token and include it in requests. If you are integrating with third-party services that expect a token format for authentication, JWTs may be required or at least convenient.

When to Avoid JWTs and Use Sessions Instead

For traditional server-rendered web applications where the same PHP application handles both the login and the protected pages, session-based authentication is usually simpler and more secure. PHP has built-in session handling that stores the session data server-side and only transmits a session identifier to the client. This eliminates the risks associated with storing identity data in a client-side token.

JWTs introduce complexity that is often unnecessary for straightforward authentication flows. You need to manage token storage, rotation, revocation, and expiry handling. With sessions, revoking access is as simple as destroying the session on the server. If you do not need stateless authentication or cross-domain identity federation, the added complexity of JWTs may not be worth the trade-off.

JWTs are also a poor choice when you need immediate revocation. If a user is compromised or you need to invalidate a token immediately, you must maintain a blocklist or use short-lived tokens with a refresh mechanism. Sessions can be invalidated instantly. If your application requires the ability to log a user out and have that revocation take effect immediately across all servers, sessions or a centralised token store are more appropriate.

Refresh Tokens and Token Rotation

Because access tokens should have short expiry times to limit the damage if one is stolen, most JWT implementations include a refresh token mechanism. When the access token expires, the client presents the refresh token to get a new access token. Refresh tokens typically have a longer lifetime, sometimes days or weeks, and are stored more securely.

Token rotation involves issuing a new refresh token each time it is used. This limits the usefulness of a stolen refresh token because it becomes invalid after use. If you are building an authentication system that handles sensitive data or financial transactions, implementing refresh token rotation adds a meaningful layer of security.

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$refreshPayload = [
    'sub' => $userId,
    'type' => 'refresh',
    'jti' => bin2hex(random_bytes(16)),
    'iat' => time(),
    'exp' => time() + 1209600
];

$refreshToken = JWT::encode($refreshPayload, $refreshSecret, 'HS256');

Store refresh tokens in your database linked to specific users. When a refresh token is used, verify it exists, has not been revoked, and matches the user making the request. After issuing a new access token, consider rotating the refresh token to limit reuse windows.

Common JWT Mistakes to Avoid

The most frequent mistake is failing to validate the algorithm. If your code accepts the algorithm from the token header without checking it against an allowed list, an attacker can change the algorithm to HS256 and sign the token using the shared secret. Always specify the expected algorithm explicitly when decoding.

Another common error is storing sensitive data in the JWT payload. The payload is base64-encoded but not encrypted. Anyone can decode it and read the contents. Never store passwords, personal data that requires protection, or anything that should not be visible to the client.

Using a weak secret key weakens the entire authentication system. Keys should be generated using a cryptographically secure random source and should be long enough to resist brute-force attacks. A 256-bit key generated with random_bytes() is appropriate for HS256.

Finally, do not skip the expiry check or issue tokens with excessively long lifetimes. A token that remains valid for months is a significant risk if it is ever compromised. For most web applications, access token lifetimes of fifteen minutes to two hours are reasonable, with refresh tokens handling longer sessions.

Security Considerations for PHP JWT Implementations

Beyond the basic validation steps, consider implementing rate limiting on authentication endpoints to reduce the risk of brute-force attacks against credentials. Log failed authentication attempts so you can detect patterns that might indicate an attack. Use HTTPS everywhere to prevent tokens from being intercepted in transit.

If your PHP application receives tokens from a client, treat every token as potentially malicious until proven otherwise. Validate the type claim if you use typed tokens to distinguish between access tokens and refresh tokens. Check the issuer and audience claims when they are present to ensure the token was issued for your application.

For applications that process payments or handle credit card data, authentication tokens are only one part of a broader security posture. The PCI DSS framework sets requirements for how authentication and data access should be handled in payment environments. Even if you are not directly subject to these requirements, reviewing the guidance can help you identify gaps in your authentication implementation.

PHP Libraries Worth Knowing

Beyond firebase/php-jwt, several other libraries handle JWT operations in PHP. lcobucci/jwt offers a more modular approach and supports a wider range of algorithms. web-token/signature-pack is part of a comprehensive framework developed by some of the original JWT specification authors. Choose a library that is actively maintained, has a clear security track record, and supports the algorithms your application requires.

When evaluating a library, check how it handles algorithm validation, whether it provides easy ways to set timeouts and other security parameters, and whether it integrates with your existing application framework. For PHP applications built with Laravel, packages like tymon/jwt-auth provide a complete JWT authentication solution tailored to Laravel's structure.

Related practical reading

These related guides can help you connect this topic with the wider website, server, security, and support decisions around it.

Making the Right Authentication Choice for Your PHP Application

JWTs are a useful authentication mechanism for specific scenarios, particularly stateless APIs, distributed systems, and cross-domain single sign-on. They are not a universal replacement for sessions, and using them inappropriately adds complexity and potential security weaknesses to your application.

Before adding JWT authentication to a PHP project, consider whether your application genuinely benefits from stateless verification, whether you can manage the complexity of token rotation and revocation, and whether sessions might be simpler and equally secure for your use case. For most traditional web applications, session-based authentication remains the more straightforward and secure option.

If you are building an API, integrating with external services, or need authentication that spans multiple application domains, JWTs are worth understanding and using correctly. The key is paying attention to the details: validating every claim, using appropriate token lifetimes, storing tokens securely on the client, and handling rotation and revocation thoughtfully.

If you are planning authentication improvements for a PHP application and want a practical review of your current setup, gather details about your existing authentication flow, the frameworks you use, and where you see the main limitations before getting in touch.

Frequently Asked Questions

What is the difference between HS256 and RS256?
HS256 uses a symmetric key, meaning the same secret is used for both signing and verification. RS256 uses asymmetric keys, with a private key for signing and a public key for verification. RS256 is generally preferred for systems where the token verifier is a different application or service, because it does not require sharing the signing key.
Can I store a JWT in a database instead of trusting it on each request?
You can, but doing so largely defeats the purpose of using JWTs. The main advantage of JWTs is stateless verification, which avoids database lookups on each request. If you need to store tokens in a database and check them on each request, sessions or opaque tokens with server-side storage may be a simpler and more secure choice.
How do I log a user out if their token has not expired?
If you need immediate logout capability, you need to maintain a token blocklist in your database or cache. When a user logs out, add the token's unique identifier (the jti claim) to the blocklist and check against it during validation. Alternatively, use very short-lived tokens and require re-authentication more frequently.
Is it safe to send a JWT in the URL?
Sending tokens in URLs is generally not recommended because URLs are logged in server access logs, browser history, and can appear in referrer headers. Tokens sent in URLs are also more likely to be leaked through browser sharing features or server-side logging. Use the Authorization header with the Bearer scheme or HTTP-only cookies instead.
How do I handle JWT expiry errors in the frontend?
Your frontend should catch 401 responses from the API, detect that the token has expired, and attempt to use the refresh token to obtain a new access token. If the refresh token is also expired or invalid, redirect the user to the login page. Designing your API to return a specific error code for token expiry rather than a generic authentication error makes this handling easier to implement.