Zum Inhalt springen

JWT - JSON Web Tokens

Der Namespace Xmf\Jwt bietet Unterstützung für JSON Web Tokens (JWT) für XOOPS-Module. JWTs ermögliche sichere, zustandslose Authentifizierung und sind besonders nützlich zum Schutz von AJAX-Anfragen.

JSON Web Tokens are a standard way to publish a set of claims (data) as a text string, with cryptographic verification that the claims have not been tampered with. For detailed specifications, see:

  • Signed: Tokens are cryptographically signed to detect tampering
  • Self-contained: All necessary information is in the token itself
  • Stateless: No server-side session storage required
  • Expirable: Tokens can include expiration times

Note: JWTs are signed, not encrypted. The data is Base64 encoded and visible. Use JWTs for integrity verification, not for hiding sensitive data.

XOOPS forms use nonce tokens for CSRF protection. However, nonces work poorly with AJAX because:

  1. Single Use: Nonces are typically valid for one submission
  2. Asynchronous Issues: Multiple AJAX requests may arrive out of order
  3. Refresh Complexity: No reliable way to refresh tokens asynchronously
  4. Context Binding: Standard tokens don’t verify which script issued them

JWTs solve these problems by:

  • Including an expiration time (exp claim) for time-limited validity
  • Supporting custom claims to bind tokens to specific scripts
  • Enabling multiple requests within the validity period
  • Providing cryptographic verification of token origin

The Xmf\Jwt\JsonWebToken class handles token creation and decoding.

use Xmf\Jwt\JsonWebToken;
use Xmf\Jwt\KeyFactory;
// Create a key
$key = KeyFactory::build('my_application_key');
// Create a JsonWebToken instance
$jwt = new JsonWebToken($key, 'HS256');
// Create a token
$payload = ['user_id' => 123, 'aud' => 'myaction'];
$token = $jwt->create($payload, 300); // Expires in 300 seconds
// Decode and verify a token
$assertClaims = ['aud' => 'myaction'];
$decoded = $jwt->decode($tokenString, $assertClaims);

new JsonWebToken($key, $algorithm)

Creates a new JWT handler.

  • $key: A Xmf\Key\KeyAbstract object
  • $algorithm: Signing algorithm (default: ‘HS256’)

create($payload, $expirationOffset)

Creates a signed token string.

  • $payload: Array of claims
  • $expirationOffset: Seconds until expiration (optional)

decode($jwtString, $assertClaims)

Decodes and validates a token.

  • $jwtString: The token to decode
  • $assertClaims: Claims to verify (empty array for none)
  • Returns: stdClass payload or false if invalid

setAlgorithm($algorithm)

Changes the signing/verification algorithm.

The Xmf\Jwt\TokenFactory provides a convenient way to create tokens.

use Xmf\Jwt\TokenFactory;
// Create a token with automatic key handling
$claims = [
'aud' => 'myaction.php',
'user_id' => $userId,
'item_id' => $itemId
];
$token = TokenFactory::build('my_key', $claims, 120);
// Token expires in 120 seconds

TokenFactory::build($key, $payload, $expirationOffset)

  • $key: Key name string or KeyAbstract object
  • $payload: Array of claims
  • $expirationOffset: Expiration in seconds

Throws exceptions on failure: DomainException, InvalidArgumentException, UnexpectedValueException

The Xmf\Jwt\TokenReader class simplifies reading tokens from various sources.

use Xmf\Jwt\TokenReader;
$assertClaims = ['aud' => 'myaction.php'];
// From a string
$payload = TokenReader::fromString('my_key', $tokenString, $assertClaims);
// From a cookie
$payload = TokenReader::fromCookie('my_key', 'token_cookie', $assertClaims);
// From a request parameter
$payload = TokenReader::fromRequest('my_key', 'token', $assertClaims);
// From Authorization header (Bearer token)
$payload = TokenReader::fromHeader('my_key', $assertClaims);

All methods return the payload as stdClass or false if invalid.

The Xmf\Jwt\KeyFactory creates and manages cryptographic keys.

use Xmf\Jwt\KeyFactory;
// Build a key (creates if it doesn't exist)
$key = KeyFactory::build('my_application_key');
// With custom storage
$storage = new \Xmf\Key\FileStorage('/custom/path');
$key = KeyFactory::build('my_key', $storage);

Keys are stored persistently. The default storage uses the file system.

Here is a complete example demonstrating JWT-protected AJAX.

<?php
use Xmf\Jwt\TokenFactory;
use Xmf\Jwt\TokenReader;
use Xmf\Module\Helper;
use Xmf\Request;
require_once dirname(dirname(__DIR__)) . '/mainfile.php';
// Claims to include and verify
$assertClaims = ['aud' => basename(__FILE__)];
// Check if this is an AJAX request
$isAjax = (0 === strcasecmp(Request::getHeader('X-Requested-With', ''), 'XMLHttpRequest'));
if ($isAjax) {
// Handle AJAX request
$GLOBALS['xoopsLogger']->activated = false;
// Verify the token from the Authorization header
$token = TokenReader::fromHeader('ajax_key', $assertClaims);
if (false === $token) {
http_response_code(401);
echo json_encode(['error' => 'Not authorized']);
exit;
}
// Token is valid - process the request
$action = Request::getCmd('action', '');
$itemId = isset($token->item_id) ? $token->item_id : 0;
// Your AJAX logic here
$response = ['success' => true, 'item_id' => $itemId];
http_response_code(200);
header('Content-Type: application/json');
echo json_encode($response);
exit;
}
// Regular page request - generate token and display page
require_once XOOPS_ROOT_PATH . '/header.php';
$helper = Helper::getHelper(basename(__DIR__));
// Create token with claims
$claims = array_merge($assertClaims, [
'item_id' => 42,
'user_id' => $GLOBALS['xoopsUser']->getVar('uid')
]);
// Token valid for 2 minutes
$token = TokenFactory::build('ajax_key', $claims, 120);
// JavaScript for AJAX calls
$script = <<<JS
<script>
function performAction(action) {
$.ajax({
url: window.location.href,
method: 'POST',
data: { action: action },
dataType: 'json',
beforeSend: function(xhr) {
xhr.setRequestHeader('Authorization', 'Bearer {$token}');
},
success: function(data) {
if (data.success) {
console.log('Action completed:', data);
// Update UI
}
},
error: function(xhr, status, error) {
if (xhr.status === 401) {
alert('Session expired. Please refresh the page.');
} else {
alert('An error occurred: ' + error);
}
}
});
}
</script>
JS;
echo $script;
echo '<button onclick="performAction(\'save\')">Save Item</button>';
echo '<button onclick="performAction(\'delete\')">Delete Item</button>';
require_once XOOPS_ROOT_PATH . '/footer.php';

Set appropriate expiration times based on use case:

// Short-lived for sensitive operations (2 minutes)
$token = TokenFactory::build('key', $claims, 120);
// Longer for general page interactions (30 minutes)
$token = TokenFactory::build('key', $claims, 1800);

Always verify the aud (audience) claim to ensure tokens are used with the intended script:

// When creating
$claims = ['aud' => 'process_order.php', 'order_id' => 123];
// When verifying
$assertClaims = ['aud' => 'process_order.php'];
$token = TokenReader::fromHeader('key', $assertClaims);

Use descriptive key names for different purposes:

// Separate keys for different features
$orderToken = TokenFactory::build('order_processing', $orderClaims, 300);
$commentToken = TokenFactory::build('comment_system', $commentClaims, 600);
use Xmf\Jwt\TokenFactory;
use Xmf\Jwt\TokenReader;
try {
$token = TokenFactory::build('my_key', $claims, 300);
} catch (\DomainException $e) {
// Invalid algorithm
error_log('JWT Error: ' . $e->getMessage());
} catch (\InvalidArgumentException $e) {
// Invalid argument
error_log('JWT Error: ' . $e->getMessage());
} catch (\UnexpectedValueException $e) {
// Unexpected value
error_log('JWT Error: ' . $e->getMessage());
}
// Reading tokens returns false on failure (no exception)
$payload = TokenReader::fromHeader('my_key', $assertClaims);
if ($payload === false) {
// Token invalid, expired, or tampered
}
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
$payload = TokenReader::fromHeader('key', $assertClaims);
// Set cookie with token
setcookie('api_token', $token, time() + 300, '/', '', true, true);
// Read from cookie
$payload = TokenReader::fromCookie('key', 'api_token', $assertClaims);
$.ajax({
url: 'handler.php',
data: { token: token, action: 'save' }
});
$payload = TokenReader::fromRequest('key', 'token', $assertClaims);
  1. Use HTTPS: Always use HTTPS to prevent token interception
  2. Short Expiration: Use the shortest practical expiration time
  3. Specific Claims: Include claims that tie tokens to specific contexts
  4. Server-Side Validation: Always validate tokens server-side
  5. Don’t Store Sensitive Data: Remember tokens are readable (not encrypted)
MethodDescription
__construct($key, $algorithm)Create JWT handler
setAlgorithm($algorithm)Set signing algorithm
create($payload, $expiration)Create signed token
decode($token, $assertClaims)Decode and verify token
MethodDescription
build($key, $payload, $expiration)Create token string
MethodDescription
fromString($key, $token, $claims)Decode from string
fromCookie($key, $name, $claims)Decode from cookie
fromRequest($key, $name, $claims)Decode from request
fromHeader($key, $claims, $header)Decode from header
MethodDescription
build($name, $storage)Get or create key
  • ../Basics/XMF-Request - Request handling
  • ../XMF-Framework - Framework overview
  • Database - Database utilities

#xmf #jwt #security #ajax #authentication #tokens