Lab: JWT Authentication with Slim
Additional Resources
Section titled “Additional Resources”- firebase/php-jwt Documentation ↗
- JSON Web Tokens
- jwt.io: Decode and inspect tokens ↗
- Slim 4 Middleware ↗
Overview
Section titled “Overview”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
AuthMiddlewareclass that intercepts requests to protected routes, validates the JWT from theAuthorizationheader, and rejects unauthorized access.
Learning Objectives
Section titled “Learning Objectives”By completing this lab, you will:
- Configure JWT settings (secret key, algorithm, expiry) in the Slim template.
- Use
firebase/php-jwtto 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.
Prerequisites
Section titled “Prerequisites”Before starting this lab, ensure you have:
- The Slim template project set up and running.
firebase/php-jwtinstalled (already included in the template).- Reviewed the JSON Web Tokens page to understand JWT structure and authentication flow.
Part I: Generating a JWT Token
Section titled “Part I: Generating a JWT Token”In this part, you will add the JWT configuration to your project and create a test route that generates a signed token.
Step 1: Add the JWT Secret Key
Section titled “Step 1: Add the JWT Secret Key”Objective: Define the JWT secret key in the environment configuration file.
Instructions:
- Open
config/env.php. - Add a JWT secret key entry. This value should be a long, random string:
$settings['jwt']['secret'] = 'your-secret-key-change-this';- Save the file.
Step 2: Add HTTP Status Constants
Section titled “Step 2: Add HTTP Status Constants”Objective: Add missing HTTP status code constants that will be used for authentication responses.
Instructions:
- Open
config/constants.php. - Add the following constants alongside the existing ones:
const HTTP_UNAUTHORIZED = 401;const HTTP_FORBIDDEN = 403;- Save the file.
Step 3: Create the AuthController
Section titled “Step 3: Create the AuthController”Objective: Create a controller that handles token generation.
Instructions:
- Create
app/Controllers/AuthController.phpextendingBaseController. - Add a constructor that accepts an
AppSettingsinstance and stores it as a private property. Callparent::__construct(). - Add a method
handleGenerateToken(Request $request, Response $response): Responsethat:- Retrieves the JWT secret from
$this->appSettings->get('jwt')['secret'] - Builds a payload with the claims:
iat,exp,iss,sub, andemail - Encodes the payload using
JWT::encode()with the secret and'HS256' - Returns the token in a JSON response using
$this->renderJson()
- Retrieves the JWT secret from
- Save the file.
Step 4: Register the Route and Test
Section titled “Step 4: Register the Route and Test”Objective: Add a route that maps to the controller method and verify it works.
Instructions:
- Open
app/Routes/routes.php. - Add a
GET /token-testroute pointing toAuthController::handleGenerateToken. - Send a
GETrequest to/token-testusing a REST client. - Copy the returned token and paste it into jwt.io ↗.
- Verify the decoded payload contains your claims (
sub,email,iat,exp,iss).
Part II: Building the AuthMiddleware
Section titled “Part II: Building the AuthMiddleware”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.
Step 5: Create the AuthMiddleware Class
Section titled “Step 5: Create the AuthMiddleware Class”Objective: Create a middleware that validates JWT tokens on protected routes.
Instructions:
- Create
app/Middleware/AuthMiddleware.php. - Use the following scaffold:
<?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. }}- Save the file.
Step 6: Register the AuthMiddleware
Section titled “Step 6: Register the AuthMiddleware”Objective: Attach the middleware to a protected route group.
Instructions:
- Open
app/Routes/routes.php. - Create a protected route group and attach
AuthMiddleware:
$app->group('/api', function (RouteCollectorProxy $group) { // Your protected routes here...})->add(AuthMiddleware::class);- Save the file.
Step 7: Test Part II
Section titled “Step 7: Test Part II”Objective: Verify the middleware protects routes and rejects unauthorized access.
Instructions:
-
No token: Send a request without an
Authorizationheader. You should receive:{ "error": "Token required" }with a
401status code. -
Invalid format: Send a request with
Authorization: InvalidFormat. You should receive:{ "error": "Invalid token format" }with a
401status code. -
Valid token: Generate a token via
/token-test, then send a request with:Authorization: Bearer <your-token>You should receive a successful response.
-
Expired token: Generate a token, temporarily set the expiry to
1second, wait, then send a request. You should receive:{ "error": "Token expired" } -
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" }