JWT - JSON Web Tokens¶
The Xmf\Jwt namespace provides JSON Web Token (JWT) support for XOOPS modules. JWTs enable secure, stateless authentication and are particularly useful for protecting AJAX requests.
What are JSON Web Tokens?¶
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:
Key Characteristics¶
- 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.
Why Use JWT in XOOPS?¶
The AJAX Token Problem¶
XOOPS forms use nonce tokens for CSRF protection. However, nonces work poorly with AJAX because:
- Single Use: Nonces are typically valid for one submission
- Asynchronous Issues: Multiple AJAX requests may arrive out of order
- Refresh Complexity: No reliable way to refresh tokens asynchronously
- Context Binding: Standard tokens don't verify which script issued them
JWT Advantages¶
JWTs solve these problems by:
- Including an expiration time (
expclaim) 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
Core Classes¶
JsonWebToken¶
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);
Methods¶
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.
TokenFactory¶
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
TokenReader¶
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.
KeyFactory¶
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.
AJAX Protection Example¶
Here is a complete example demonstrating JWT-protected AJAX.
Page Script (Generates Token)¶
<?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';
Best Practices¶
Token Expiration¶
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);
Claim Verification¶
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);
Key Naming¶
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);
Error Handling¶
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
}
Token Transport Methods¶
Authorization Header (Recommended)¶
Cookie¶
// Set cookie with token
setcookie('api_token', $token, time() + 300, '/', '', true, true);
// Read from cookie
$payload = TokenReader::fromCookie('key', 'api_token', $assertClaims);
Request Parameter¶
Security Considerations¶
- Use HTTPS: Always use HTTPS to prevent token interception
- Short Expiration: Use the shortest practical expiration time
- Specific Claims: Include claims that tie tokens to specific contexts
- Server-Side Validation: Always validate tokens server-side
- Don't Store Sensitive Data: Remember tokens are readable (not encrypted)
API Reference¶
Xmf\Jwt\JsonWebToken¶
| Method | Description |
|---|---|
__construct($key, $algorithm) | Create JWT handler |
setAlgorithm($algorithm) | Set signing algorithm |
create($payload, $expiration) | Create signed token |
decode($token, $assertClaims) | Decode and verify token |
Xmf\Jwt\TokenFactory¶
| Method | Description |
|---|---|
build($key, $payload, $expiration) | Create token string |
Xmf\Jwt\TokenReader¶
| Method | Description |
|---|---|
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 |
Xmf\Jwt\KeyFactory¶
| Method | Description |
|---|---|
build($name, $storage) | Get or create key |
See Also¶
- XMF-Request - Request handling
- XMF-Framework - Framework overview
- Database - Database utilities