PHP cookbook · Intermediate
Protect a PHP API with JWT bearer tokens
Validate JWT access tokens on every API request and expose the authenticated user to controllers.
Approach
Use a maintained library (firebase/php-jwt or lcobucci/jwt) instead of hand-rolled parsing. Read the Authorization: Bearer header, verify the signature with a pinned algorithm and key, check exp, iss and aud, then load the user. Return 401 with WWW-Authenticate: Bearer on failure. Keep access tokens short-lived.
Solution
<?php
declare(strict_types=1);
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class JwtAuthMiddleware implements MiddlewareInterface
{
public function __construct(private string $publicKey, private string $issuer, private string $audience,
private UserRepository $users, private ResponseFactory $responses) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$header = $request->getHeaderLine('Authorization');
if (!str_starts_with($header, 'Bearer ')) {
return $this->unauthorized('Missing bearer token');
}
try {
JWT::$leeway = 30; // seconds of clock skew
$claims = JWT::decode(substr($header, 7), new Key($this->publicKey, 'RS256'));
} catch (Throwable) {
return $this->unauthorized('Invalid or expired token');
}
$aud = (array) ($claims->aud ?? []);
if (($claims->iss ?? null) !== $this->issuer || !in_array($this->audience, $aud, true)) {
return $this->unauthorized('Token not issued for this API');
}
$user = $this->users->find((int) $claims->sub) ?? null;
if ($user === null) {
return $this->unauthorized('Unknown user');
}
return $handler->handle($request->withAttribute('user', $user));
}
private function unauthorized(string $message): ResponseInterface
{
return $this->responses->problem(401, $message)->withHeader('WWW-Authenticate', 'Bearer');
}
}
Pitfalls
- Accepting the algorithm from the token header.
- Long-lived access tokens without a revocation strategy.
- Putting sensitive data in claims — JWT payloads are readable.
Variations
- Laravel: Sanctum (opaque tokens/cookies) or Passport (OAuth2).
- Symfony: lexik/jwt-authentication-bundle or the built-in access token authenticator with an OIDC handler.
Further reading: firebase/php-jwt
Reference: https://github.com/firebase/php-jwt
More PHP recipes
- Add CSRF protection to PHP forms
- Add health check endpoints to a PHP app
- Add structured logging with request IDs in PHP
- Build a secure password reset flow in PHP
- Cache expensive queries with Redis in PHP
- Call external APIs with timeouts and retries in PHP
- Handle file uploads securely in PHP
- Implement rate limiting in a PHP API