diff --git a/composer.json b/composer.json index b573d70..7d547af 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,12 @@ { - "name": "developersrede/erede-php", - "version": "5.2.1", + "name": "robsondrs/erede-php", + "version": "5.2.5", "description": "e.Rede integration SDK", "minimum-stability": "stable", "license": "MIT", "type": "library", "require": { - "php": "^8.1", + "php": "^7.3", "ext-curl": "*", "ext-json": "*", "psr/log": "*", @@ -28,6 +28,10 @@ { "name": "João Batista Neto", "email": "neto.joaobatista@gmail.com" + }, + { + "name": "Robson Soares", + "email": "robson_drs@hotmail.com" } ] } diff --git a/src/Rede/Environment.php b/src/Rede/Environment.php index 97694a7..ec3e4fd 100644 --- a/src/Rede/Environment.php +++ b/src/Rede/Environment.php @@ -10,6 +10,12 @@ class Environment implements RedeSerializable public const SANDBOX = 'https://api.userede.com.br/desenvolvedores'; public const VERSION = 'v1'; + /** + * OAuth2 token endpoints + */ + public const OAUTH_TOKEN_PRODUCTION = 'https://api.userede.com.br/redelabs/oauth2/token'; + public const OAUTH_TOKEN_SANDBOX = 'https://rl7-sandbox-api.useredecloud.com.br/oauth2/token'; + /** * @var string|null */ @@ -25,6 +31,11 @@ class Environment implements RedeSerializable */ private string $endpoint; + /** + * @var string OAuth2 token endpoint URL + */ + private string $oauthTokenUrl; + /** * Creates an environment with its base url and version * @@ -33,6 +44,14 @@ class Environment implements RedeSerializable private function __construct(string $baseUrl) { $this->endpoint = sprintf('%s/%s/', $baseUrl, Environment::VERSION); + + if ($baseUrl === Environment::PRODUCTION) { + $this->oauthTokenUrl = Environment::OAUTH_TOKEN_PRODUCTION; + } elseif ($baseUrl === Environment::SANDBOX) { + $this->oauthTokenUrl = Environment::OAUTH_TOKEN_SANDBOX; + } else { + $this->oauthTokenUrl = rtrim($baseUrl, '/') . '/oauth2/token'; + } } /** @@ -61,6 +80,24 @@ public function getEndpoint(string $service): string return $this->endpoint . $service; } + /** + * @return string OAuth2 token endpoint URL + */ + public function getOAuthTokenUrl(): string + { + return $this->oauthTokenUrl; + } + + /** + * @param string $oauthTokenUrl + * @return $this + */ + public function setOAuthTokenUrl(string $oauthTokenUrl): static + { + $this->oauthTokenUrl = $oauthTokenUrl; + return $this; + } + /** * @return string|null */ diff --git a/src/Rede/Service/AbstractService.php b/src/Rede/Service/AbstractService.php index 0a9af10..376196d 100644 --- a/src/Rede/Service/AbstractService.php +++ b/src/Rede/Service/AbstractService.php @@ -85,11 +85,11 @@ protected function sendRequest(string $body = '', string $method = 'GET'): Trans throw new RuntimeException('Was not possible to create a curl instance.'); } - curl_setopt( - $curl, - CURLOPT_USERPWD, - sprintf('%s:%s', $this->store->getFiliation(), $this->store->getToken()) - ); + // OAuth2 Bearer Token authentication flow + $bearer = $this->ensureBearerToken(); + if ($bearer !== null) { + $headers[] = 'Authorization: Bearer ' . $bearer; + } curl_setopt($curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); @@ -223,4 +223,90 @@ private function dumpHttpInfo(array $httpInfo): void * @return Transaction */ abstract protected function parseResponse(string $response, int $statusCode): Transaction; + + /** + * Ensures a valid Bearer Token, obtaining a new one via OAuth2 when needed. + * + * @return string|null + */ + private function ensureBearerToken(): ?string + { + $currentToken = $this->store->getBearerToken(); + $expiresAt = $this->store->getBearerTokenExpiresAt(); + + $now = time(); + if ($currentToken !== null && is_int($expiresAt) && $expiresAt > ($now + 60)) { + return $currentToken; + } + + return $this->requestBearerToken(); + } + + /** + * Requests a new token via OAuth2 (client_credentials) and stores it in the Store. + * + * @return string|null + */ + private function requestBearerToken(): ?string + { + $env = $this->store->getEnvironment(); + $tokenUrl = $env->getOAuthTokenUrl(); + + $this->logger?->debug(sprintf('Requesting OAuth2 token at %s', $tokenUrl)); + + $curl = curl_init($tokenUrl); + if (!$curl instanceof CurlHandle) { + throw new RuntimeException('Was not possible to create a curl instance for OAuth token.'); + } + + $basic = base64_encode(sprintf('%s:%s', $this->store->getFiliation(), $this->store->getToken())); + + $headers = [ + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded', + 'Authorization: Basic ' . $basic, + ]; + + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query(['grant_type' => 'client_credentials'])); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); + + $response = curl_exec($curl); + $httpInfo = curl_getinfo($curl); + + if (curl_errno($curl)) { + $errorMessage = sprintf('Curl error on OAuth token request[%s]: %s', curl_errno($curl), curl_error($curl)); + curl_close($curl); + throw new RuntimeException($errorMessage); + } + + curl_close($curl); + + if (!is_string($response)) { + throw new RuntimeException('Invalid OAuth token response'); + } + + $this->logger?->debug(sprintf("OAuth token response status=%s body=%s", $httpInfo['http_code'] ?? 'n/a', $response)); + + $decoded = json_decode($response, true); + if (!is_array($decoded)) { + throw new RuntimeException('Unable to parse OAuth token response'); + } + + $accessToken = $decoded['access_token'] ?? null; + $expiresIn = $decoded['expires_in'] ?? 0; + + if (!is_string($accessToken) || $accessToken === '') { + $errorDescription = $decoded['error_description'] ?? $decoded['error'] ?? 'Unknown error obtaining access token'; + throw new RuntimeException(sprintf('OAuth token error: %s', $errorDescription)); + } + + $expiresAt = time() + (is_int($expiresIn) ? $expiresIn : (int)$expiresIn) - 60; // margem de segurança + $this->store->setBearerToken($accessToken, $expiresAt); + + return $accessToken; + } } diff --git a/src/Rede/Store.php b/src/Rede/Store.php index ef56f19..d68f953 100644 --- a/src/Rede/Store.php +++ b/src/Rede/Store.php @@ -10,6 +10,16 @@ class Store */ private Environment $environment; + /** + * @var string|null Bearer token obtained via OAuth2 + */ + private ?string $bearerToken = null; + + /** + * @var int|null Epoch expiration of the token + */ + private ?int $bearerTokenExpiresAt = null; + /** * Creates a store. * @@ -78,4 +88,34 @@ public function setToken(string $token): static $this->token = $token; return $this; } + + /** + * Define the bearer token and the absolute expiration time (epoch). + * + * @param string $token + * @param int $expiresAtEpoch + * @return $this + */ + public function setBearerToken(string $token, int $expiresAtEpoch): static + { + $this->bearerToken = $token; + $this->bearerTokenExpiresAt = $expiresAtEpoch; + return $this; + } + + /** + * @return string|null + */ + public function getBearerToken(): ?string + { + return $this->bearerToken; + } + + /** + * @return int|null + */ + public function getBearerTokenExpiresAt(): ?int + { + return $this->bearerTokenExpiresAt; + } }