settingsGateway->getAuthMode() !== HelpdeskSettingsGateway::AUTH_MODE_OAUTH2) { return null; } if ($tenantId <= 0) { return null; } // Check cache first $cached = $this->tokenRepository->findValidToken($tenantId); if ($cached !== null) { return $cached; } $clientId = trim((string) ($this->settingsGateway->getOAuthClientId() ?? '')); $clientSecret = trim((string) ($this->settingsGateway->getOAuthClientSecret() ?? '')); $tokenEndpoint = $this->resolveTokenEndpoint(); if ($clientId === '' || $clientSecret === '' || $tokenEndpoint === '') { return null; } $tokenResponse = $this->requestToken($tokenEndpoint, $clientId, $clientSecret, $resourceAudience); if (!is_array($tokenResponse)) { return null; } $accessToken = trim((string) ($tokenResponse['access_token'] ?? '')); if ($accessToken === '') { return null; } $expiresIn = (int) ($tokenResponse['expires_in'] ?? 0); if ($expiresIn <= 0) { $expiresIn = (int) ($tokenResponse['ext_expires_in'] ?? 0); } if ($expiresIn <= 0) { $expiresIn = 300; } $expiresAt = new \DateTimeImmutable('+' . max(60, $expiresIn - 60) . ' seconds'); $this->tokenRepository->storeToken($tenantId, $accessToken, $expiresAt); return $accessToken; } private function resolveTokenEndpoint(): string { $endpoint = trim((string) ($this->settingsGateway->getOAuthTokenEndpoint() ?? '')); if ($endpoint === '') { return ''; } $tenantId = trim((string) ($this->settingsGateway->getOAuthTenantId() ?? '')); if ($tenantId !== '') { $endpoint = str_replace(['{tenant}', '{tenant_id}'], $tenantId, $endpoint); } return $endpoint; } /** * @return array|null */ private function requestToken(string $endpoint, string $clientId, string $clientSecret, ?string $resourceAudience): ?array { $payloads = $this->buildPayloadCandidates($endpoint, $clientId, $clientSecret, $resourceAudience); foreach ($payloads as $payload) { $result = $this->requestTokenWithPayload($endpoint, $payload); if ($result !== null) { return $result; } } return null; } /** * @return array> */ private function buildPayloadCandidates(string $endpoint, string $clientId, string $clientSecret, ?string $resourceAudience): array { $basePayload = [ 'grant_type' => 'client_credentials', 'client_id' => $clientId, 'client_secret' => $clientSecret, ]; $payloads = []; $isV2Endpoint = str_contains(strtolower($endpoint), '/oauth2/v2.0/token'); $audience = trim((string) ($resourceAudience ?? '')); if ($isV2Endpoint) { $scopes = ['https://api.businesscentral.dynamics.com/.default']; if ($audience !== '') { $scopes[] = rtrim($audience, '/') . '/.default'; } foreach (array_values(array_unique($scopes)) as $scope) { $payloads[] = $basePayload + ['scope' => $scope]; } } else { $resources = ['https://api.businesscentral.dynamics.com']; if ($audience !== '') { $resources[] = $audience; } foreach (array_values(array_unique($resources)) as $resource) { $payloads[] = $basePayload + ['resource' => $resource]; } } $payloads[] = $basePayload; return $payloads; } /** * @param array $payload * @return array|null */ private function requestTokenWithPayload(string $endpoint, array $payload): ?array { $ch = curl_init(); if ($ch === false) { return null; } curl_setopt_array($ch, [ CURLOPT_URL => $endpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT, CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT, CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'], CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($payload, '', '&', PHP_QUERY_RFC3986), CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $responseBody = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); unset($ch); if (!is_string($responseBody) || $httpCode < 200 || $httpCode >= 300) { return null; } $decoded = json_decode($responseBody, true); if (!is_array($decoded)) { return null; } return $decoded; } }