Enhanced the VEO3 usage report system to support all AI tool types: - Added support for 6 tool types: VEO3, TEXT2IMAGE, TEXT2VOICE, SPEECH2SPEECH, DOCUMENT_TRANSLATION, VIDEOQUERY - Updated Python report generator (veo3_report.py) with dynamic tool detection - Updated PHP report page (report.php) with tool usage breakdown section - Changed all "Prompts" references to "Requests" for clarity - Updated refresh button to fetch only last 12 weeks (84 days) for better performance - Made system dynamic to handle unknown tool types automatically - Renamed report titles from "VEO3 Usage Report" to "AI Tools Usage Report" Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
201 lines
6.3 KiB
PHP
201 lines
6.3 KiB
PHP
<?php
|
|
/**
|
|
* JWT Token Validator for Azure AD / MSAL
|
|
* Validates JWT tokens from Microsoft Azure AD using JWKS
|
|
*/
|
|
|
|
// Load Composer autoload for Firebase JWT library
|
|
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
|
|
require_once __DIR__ . '/vendor/autoload.php';
|
|
}
|
|
|
|
use Firebase\JWT\JWT;
|
|
use Firebase\JWT\JWK;
|
|
|
|
class JWTValidator {
|
|
private $tenantId;
|
|
private $clientId;
|
|
private $jwksCache = null;
|
|
private $jwksCacheTime = 0;
|
|
private $jwksCacheDuration = 3600; // Cache for 1 hour
|
|
|
|
public function __construct($tenantId, $clientId) {
|
|
$this->tenantId = $tenantId;
|
|
$this->clientId = $clientId;
|
|
}
|
|
|
|
/**
|
|
* Validate a JWT token
|
|
*
|
|
* @param string $token The JWT token to validate
|
|
* @return array ['valid' => bool, 'payload' => array|null, 'error' => string|null]
|
|
*/
|
|
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 from Azure AD'];
|
|
}
|
|
|
|
// Convert JWKS to Key objects
|
|
$keys = JWK::parseKeySet($jwks);
|
|
|
|
// Decode and validate the JWT
|
|
$decoded = JWT::decode($token, $keys);
|
|
$payload = (array) $decoded;
|
|
|
|
// Validate claims
|
|
$validation = $this->validateClaims($payload);
|
|
if (!$validation['valid']) {
|
|
return $validation;
|
|
}
|
|
|
|
return ['valid' => true, 'payload' => $payload];
|
|
|
|
} catch (\Firebase\JWT\ExpiredException $e) {
|
|
return ['valid' => false, 'error' => 'Token has expired'];
|
|
} catch (\Firebase\JWT\SignatureInvalidException $e) {
|
|
return ['valid' => false, 'error' => 'Token signature is invalid'];
|
|
} catch (\Firebase\JWT\BeforeValidException $e) {
|
|
return ['valid' => false, 'error' => 'Token is not yet valid'];
|
|
} catch (Exception $e) {
|
|
return ['valid' => false, 'error' => 'JWT validation failed: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate JWT claims
|
|
*
|
|
* @param array $payload The decoded JWT payload
|
|
* @return array ['valid' => bool, 'error' => string|null]
|
|
*/
|
|
private function validateClaims($payload) {
|
|
$now = time();
|
|
|
|
// Check expiration (exp claim)
|
|
if (isset($payload['exp']) && $payload['exp'] < $now) {
|
|
return ['valid' => false, 'error' => 'Token has expired'];
|
|
}
|
|
|
|
// Check not-before (nbf claim)
|
|
if (isset($payload['nbf']) && $payload['nbf'] > $now) {
|
|
return ['valid' => false, 'error' => 'Token is not yet valid'];
|
|
}
|
|
|
|
// Validate audience (aud claim) - must be our client ID or 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']];
|
|
}
|
|
}
|
|
|
|
// Validate issuer (iss claim) - must be from Azure AD tenant
|
|
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 (JSON Web Key Set) from Azure AD
|
|
*
|
|
* @return array|null
|
|
*/
|
|
private function getJWKS() {
|
|
// Return cached JWKS if still valid
|
|
if ($this->jwksCache && (time() - $this->jwksCacheTime) < $this->jwksCacheDuration) {
|
|
return $this->jwksCache;
|
|
}
|
|
|
|
// Get OpenID configuration from Azure AD
|
|
$configUrl = "https://login.microsoftonline.com/{$this->tenantId}/v2.0/.well-known/openid-configuration";
|
|
|
|
$config = $this->fetchJson($configUrl);
|
|
if (!$config || !isset($config['jwks_uri'])) {
|
|
error_log("Failed to get OpenID configuration from: $configUrl");
|
|
return null;
|
|
}
|
|
|
|
// Fetch JWKS from jwks_uri
|
|
$jwks = $this->fetchJson($config['jwks_uri']);
|
|
|
|
if (!$jwks || !isset($jwks['keys'])) {
|
|
error_log("Failed to get JWKS from: " . $config['jwks_uri']);
|
|
return null;
|
|
}
|
|
|
|
// Ensure all keys have the 'alg' parameter
|
|
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';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache the JWKS
|
|
$this->jwksCache = $jwks;
|
|
$this->jwksCacheTime = time();
|
|
|
|
return $jwks;
|
|
}
|
|
|
|
/**
|
|
* Fetch JSON from a URL
|
|
*
|
|
* @param string $url
|
|
* @return array|null
|
|
*/
|
|
private function fetchJson($url) {
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlError = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($curlError) {
|
|
error_log("CURL Error fetching $url: $curlError");
|
|
return null;
|
|
}
|
|
|
|
if ($httpCode !== 200) {
|
|
error_log("HTTP Error $httpCode fetching $url");
|
|
return null;
|
|
}
|
|
|
|
$json = json_decode($response, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
error_log("JSON decode error fetching $url: " . json_last_error_msg());
|
|
return null;
|
|
}
|
|
|
|
return $json;
|
|
}
|
|
}
|