format/JWTValidator.php
2025-09-08 16:25:26 -05:00

143 lines
No EOL
4.4 KiB
PHP

<?php
require_once __DIR__ . '/vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\JWK;
class JWTValidator {
private $tenantId;
private $clientId;
public function __construct($tenantId, $clientId) {
$this->tenantId = $tenantId;
$this->clientId = $clientId;
}
/**
* Validate JWT token from Azure AD using Firebase JWT
*/
public function validateToken($token) {
if (empty($token)) {
return ['valid' => false, 'error' => 'Token is empty'];
}
try {
// Get public keys from Azure AD
$jwks = $this->getJWKS();
if (!$jwks) {
return ['valid' => false, 'error' => 'Could not retrieve public keys'];
}
// Convert JWKS to Key objects
$keys = JWK::parseKeySet($jwks);
// Decode and validate the JWT
$decoded = JWT::decode($token, $keys);
// Convert to array for easier handling
$payload = (array) $decoded;
// Additional claim validation
$validation = $this->validateClaims($payload);
if (!$validation['valid']) {
return $validation;
}
return ['valid' => true, 'payload' => $payload];
} catch (Exception $e) {
return ['valid' => false, 'error' => 'JWT validation failed: ' . $e->getMessage()];
}
}
/**
* Validate token claims
*/
private function validateClaims($payload) {
$now = time();
// Check expiration
if (isset($payload['exp']) && $payload['exp'] < $now) {
return ['valid' => false, 'error' => 'Token has expired'];
}
// Check not before
if (isset($payload['nbf']) && $payload['nbf'] > $now) {
return ['valid' => false, 'error' => 'Token is not yet valid'];
}
// Check audience - for ID tokens, audience should be our client ID
// For access tokens, it could be Microsoft Graph
if (isset($payload['aud'])) {
$validAudiences = [
$this->clientId,
'00000003-0000-0000-c000-000000000000', // Microsoft Graph
'https://graph.microsoft.com'
];
if (!in_array($payload['aud'], $validAudiences)) {
return ['valid' => false, 'error' => 'Invalid audience: ' . $payload['aud']];
}
}
// Check issuer - accept multiple valid formats
if (isset($payload['iss'])) {
$validIssuers = [
"https://login.microsoftonline.com/{$this->tenantId}/v2.0",
"https://login.microsoftonline.com/{$this->tenantId}/",
"https://sts.windows.net/{$this->tenantId}/",
"https://login.microsoftonline.com/common/v2.0"
];
if (!in_array($payload['iss'], $validIssuers)) {
return ['valid' => false, 'error' => 'Invalid issuer: ' . $payload['iss']];
}
}
return ['valid' => true];
}
/**
* Get JWKS from Azure AD
*/
private function getJWKS() {
$configUrl = "https://login.microsoftonline.com/{$this->tenantId}/v2.0/.well-known/openid-configuration";
$config = $this->fetchJson($configUrl);
if (!$config || !isset($config['jwks_uri'])) {
return null;
}
$jwks = $this->fetchJson($config['jwks_uri']);
// Ensure all keys have the 'alg' parameter
if ($jwks && isset($jwks['keys'])) {
foreach ($jwks['keys'] as &$key) {
if (!isset($key['alg'])) {
// Default to RS256 for RSA keys
if (isset($key['kty']) && $key['kty'] === 'RSA') {
$key['alg'] = 'RS256';
}
}
}
}
return $jwks;
}
/**
* Fetch JSON from URL
*/
private function fetchJson($url) {
$context = stream_context_create([
'http' => [
'timeout' => 10,
'user_agent' => 'Azure-AD-JWT-Validator/1.0'
]
]);
$response = file_get_contents($url, false, $context);
return $response ? json_decode($response, true) : null;
}
}