143 lines
No EOL
4.4 KiB
PHP
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;
|
|
}
|
|
|
|
} |