Skip to content

Lab: JWT Authentication with Slim


In this lab, you will implement JWT-based authentication for your Slim REST API using the firebase/php-jwt library.

  • In Part I, you will configure JWT settings and create a test route that generates a signed JWT token.
  • In Part II, you will build an AuthMiddleware class that intercepts requests to protected routes, validates the JWT from the Authorization header, and rejects unauthorized access.

By completing this lab, you will:

  • Configure JWT settings (secret key, algorithm, expiry) in the Slim template.
  • Use firebase/php-jwt to encode and decode JWT tokens.
  • Build a PSR-15 middleware that validates Bearer tokens on incoming requests.
  • Handle authentication errors by returning appropriate HTTP status codes.
  • Pass decoded token claims to downstream middleware and route handlers using request attributes.

Before starting this lab, ensure you have:

  • The Slim template project set up and running.
  • firebase/php-jwt installed (already included in the template).
  • Reviewed the JSON Web Tokens page to understand JWT structure and authentication flow.

In this part, you will add the JWT configuration to your project and create a test route that generates a signed token.

Objective: Define the JWT secret key in the environment configuration file.

Instructions:

  1. Open config/env.php.
  2. Add a JWT secret key entry. This value should be a long, random string:
config/env.php
$settings['jwt']['secret'] = 'your-secret-key-change-this';
  1. Save the file.

Objective: Add missing HTTP status code constants that will be used for authentication responses.

Instructions:

  1. Open config/constants.php.
  2. Add the following constants alongside the existing ones:
config/constants.php
const HTTP_UNAUTHORIZED = 401;
const HTTP_FORBIDDEN = 403;
  1. Save the file.

Objective: Create a controller that handles token generation.

Instructions:

  1. Create app/Controllers/AuthController.php extending BaseController.
  2. Add a constructor that accepts an AppSettings instance and stores it as a private property. Call parent::__construct().
  3. Add a method handleGenerateToken(Request $request, Response $response): Response that:
    • Retrieves the JWT secret from $this->appSettings->get('jwt')['secret']
    • Builds a payload with the claims: iat, exp, iss, sub, and email
    • Encodes the payload using JWT::encode() with the secret and 'HS256'
    • Returns the token in a JSON response using $this->renderJson()
  4. Save the file.

Objective: Add a route that maps to the controller method and verify it works.

Instructions:

  1. Open app/Routes/routes.php.
  2. Add a GET /token-test route pointing to AuthController::handleGenerateToken.
  3. Send a GET request to /token-test using a REST client.
  4. Copy the returned token and paste it into jwt.io.
  5. Verify the decoded payload contains your claims (sub, email, iat, exp, iss).

In this part, you will create a middleware that intercepts incoming requests, extracts the JWT from the Authorization header, and validates it before allowing access to protected routes.

Objective: Create a middleware that validates JWT tokens on protected routes.

Instructions:

  1. Create app/Middleware/AuthMiddleware.php.
  2. Use the following scaffold:
app/Middleware/AuthMiddleware.php
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Helpers\Core\AppSettings;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Exception\HttpUnauthorizedException;
class AuthMiddleware implements MiddlewareInterface
{
private AppSettings $appSettings;
public function __construct(AppSettings $appSettings)
{
$this->appSettings = $appSettings;
}
public function process(Request $request, RequestHandler $handler): ResponseInterface
{
// TODO 1: Get the JWT secret and algorithm from $this->appSettings->get('jwt').
// TODO 2: Get the Authorization header from the request.
// If the header is missing or empty, throw an HttpUnauthorizedException
// with the message "Token required".
// TODO 3: Extract the token from the header.
// The expected format is "Bearer <token>".
// If the format is invalid, throw an HttpUnauthorizedException
// with the message "Invalid token format".
// TODO 4: Decode and validate the token using JWT::decode().
// Pass the token along with a new Key object: new Key($secret, $algorithm).
// Wrap this in a try/catch block to handle:
// - ExpiredException: throw HttpUnauthorizedException with "Token expired"
// - SignatureInvalidException: throw HttpUnauthorizedException with "Invalid token signature"
// - Any other exception: throw HttpUnauthorizedException with "Invalid token"
// TODO 5: If decoding succeeds, attach the decoded claims to the request
// using withAttribute(). Add at minimum:
// - 'token_user_id' from the 'sub' claim
// - 'token_email' from the 'email' claim
// IMPORTANT: These calls must come BEFORE calling $handler->handle().
// TODO 6: Pass the modified request to the next handler and return the response.
}
}
  1. Save the file.

Objective: Attach the middleware to a protected route group.

Instructions:

  1. Open app/Routes/routes.php.
  2. Create a protected route group and attach AuthMiddleware:
app/Routes/routes.php
$app->group('/api', function (RouteCollectorProxy $group) {
// Your protected routes here...
})->add(AuthMiddleware::class);
  1. Save the file.

Objective: Verify the middleware protects routes and rejects unauthorized access.

Instructions:

  1. No token: Send a request without an Authorization header. You should receive:

    { "error": "Token required" }

    with a 401 status code.

  2. Invalid format: Send a request with Authorization: InvalidFormat. You should receive:

    { "error": "Invalid token format" }

    with a 401 status code.

  3. Valid token: Generate a token via /token-test, then send a request with:

    Authorization: Bearer <your-token>

    You should receive a successful response.

  4. Expired token: Generate a token, temporarily set the expiry to 1 second, wait, then send a request. You should receive:

    { "error": "Token expired" }
  5. Tampered token: Copy a valid token, change a few characters in the payload section (the middle part between the dots), and send it. You should receive:

    { "error": "Invalid token signature" }