Skip to Content
Course content

178: Building a REST API with JWT Authentication

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen a lot of developers hit a wall the first time they implement JWTs in PHP. They've got their library installed, their secret key set, and they're sending the token from the frontend—but the server keeps screaming "Invalid Token" or returning a 401 Unauthorized. Usually, the code looks something like this:

// The problematic code
$headers = apache_request_headers();
$token = $headers['Authorization'] ?? null;

if ($token) {
    try {
        // This is where it crashes
        $decoded = JWT::decode($token, new Key($secret_key, 'HS256'));
        echo "Welcome, " . $decoded->user_id;
    } catch (Exception $e) {
        http_response_code(401);
        echo "Invalid token: " . $e->getMessage();
    }
}

The "Bearer" String Trap

If you run this, it will fail every single time. Why? Because of the HTTP standard. When a frontend client sends a JWT, they don't just send the raw string; they send it in the format Authorization: Bearer eyJhbGci....

The JWT::decode method is expecting just the encoded string. It doesn't know what "Bearer " is. It sees those first seven characters, realizes they aren't part of a valid Base64Url encoded JWT header, and throws an exception. I spent way too many hours debugging this early in my career because I assumed the library handled the header prefix for me. It doesn't.

Cleaning the Token Header

To fix this, you need to strip that prefix before passing the string to the decoder. The most reliable way is to check if the string starts with "Bearer " and then slice it off. Here is how I usually handle it:

$headers = apache_request_headers();
$authHeader = $headers['Authorization'] ?? '';

// Use a regex or a simple string check to extract the actual token
if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
    $token = $matches[1];
    
    try {
        $decoded = JWT::decode($token, new Key($secret_key, 'HS256'));
        // Now $decoded contains your payload
    } catch (Exception $e) {
        http_response_code(401);
        echo json_encode(["error" => "Token is expired or invalid"]);
    }
} else {
    http_response_code(401);
    echo json_encode(["error" => "Authorization header not found or malformed"]);
}

By using preg_match, we ensure that we're only grabbing the non-whitespace characters following the word "Bearer". This makes your API much more robust.

Issuing Tokens with Expiration

When you're building the login endpoint, you can't just put the user ID in the token. If that token is stolen, it's valid forever. You need to include an exp (expiration) claim. The firebase/php-jwt library looks for this specific key to automatically invalidate tokens that have lived too long.

I recommend keeping access tokens short-lived—maybe 15 to 60 minutes. Here is a clean way to structure your payload:

$payload = [
    'iss' => 'your-api-domain.com',    // Issuer
    'aud' => 'your-api-domain.com',    // Audience
    'iat' => time(),                   // Issued at
    'nbf' => time(),                   // Not before
    'exp' => time() + 3600,            // Expires in 1 hour
    'data' => [                        // Your custom user data
        'userId' => $user['id'],
        'role' => $user['role']
    ]
];

$jwt = JWT::encode($payload, $secret_key, 'HS256');

Protecting Your API Routes

You don't want to paste that decoding logic into every single file. Instead, wrap it in a middleware function or a helper class. In a REST API, your "Guard" should happen before any controller logic is executed. If the token is missing or invalid, kill the request immediately with a 401 Unauthorized. Only if the token is valid should you attach the userId to the request object and allow the code to proceed to the database queries.

One final tip: Never put sensitive data like passwords or credit card numbers inside a JWT. Remember that JWTs are encoded, not encrypted. Anyone with the token can paste it into a tool like jwt.io and see exactly what's inside the payload. The signature only prevents them from changing the data; it doesn't hide it.




📋 Practical Task

Build a Protected Secure-Note Storage API

Your task is to create a mini-REST API that allows users to save private notes. You will need to implement the following:

  • A /login.php endpoint: This should accept a username and password. If valid, return a JWT containing the user_id and an expiration time of 30 minutes.
  • A /notes.php endpoint: This endpoint must be protected. It should check for the Authorization: Bearer [token] header. If the token is valid, return a JSON list of notes associated with that user_id. If the token is missing, expired, or malformed, return a 401 status code.
  • Implementation Detail: Use a hardcoded secret key for now, but store it in a separate variable at the top of your scripts to simulate an environment variable.

Testing Hint: Use Postman or Insomnia to send the "Authorization" header. Make sure you include the word "Bearer " before your token string.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.