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; } }