From a0051ac18ca4e73718213feebae671a5f73b901b Mon Sep 17 00:00:00 2001 From: Robson Date: Sun, 5 Oct 2025 18:08:20 -0300 Subject: [PATCH 1/3] =?UTF-8?q?Adiciona=20suporte=20a=20autentica=C3=A7?= =?UTF-8?q?=C3=A3o=20OAuth2=20com=20Bearer=20Token=20e=20endpoints=20para?= =?UTF-8?q?=20obten=C3=A7=C3=A3o=20de=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Rede/Environment.php | 37 +++++++++++ src/Rede/Service/AbstractService.php | 96 ++++++++++++++++++++++++++-- src/Rede/Store.php | 40 ++++++++++++ 3 files changed, 168 insertions(+), 5 deletions(-) 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; + } } From b446bfc762ca50927c1381e8afb2ff919b9a65c7 Mon Sep 17 00:00:00 2001 From: Robson Date: Thu, 4 Dec 2025 17:02:19 -0300 Subject: [PATCH 2/3] =?UTF-8?q?Atualiza=20nome=20do=20pacote=20e=20vers?= =?UTF-8?q?=C3=A3o=20para=205.2.5,=20adiciona=20novo=20colaborador=20Robso?= =?UTF-8?q?n=20Soares.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b573d70..790a79e 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "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", @@ -28,6 +28,10 @@ { "name": "João Batista Neto", "email": "neto.joaobatista@gmail.com" + }, + { + "name": "Robson Soares", + "email": "robson_drs@hotmail.com" } ] } From c113e839cc4d98f7d7061db6c83b847ee264dcc7 Mon Sep 17 00:00:00 2001 From: Robson da Rosa Soares Date: Thu, 4 Dec 2025 17:16:08 -0300 Subject: [PATCH 3/3] Change PHP requirement from ^8.1 to ^7.3 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 790a79e..7d547af 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "type": "library", "require": { - "php": "^8.1", + "php": "^7.3", "ext-curl": "*", "ext-json": "*", "psr/log": "*",