refactor(social): single source of truth for token redaction

Same shape of bug as the refreshToken duplication: the regex that strips
access_token / Bearer headers from logged HTTP bodies existed in three
near-identical copies (HasSocialHttpClient trait, TokenRefreshClient,
SocialPublishException), drifting subtly — SocialPublishException was
missing the JSON "token" pattern.

Extracts a TokenRedactor::redact(string) helper and routes all callers
through it. Adding a new token format now means one regex in one file.
This commit is contained in:
Paulo Castellano 2026-05-19 09:21:43 -03:00
parent fcb70b3599
commit cf80e1fcae
4 changed files with 38 additions and 58 deletions

View file

@ -4,6 +4,7 @@
namespace App\Exceptions\Social;
use App\Services\Social\TokenRedactor;
use RuntimeException;
abstract class SocialPublishException extends RuntimeException
@ -27,32 +28,10 @@ public function context(): array
'category' => $this->category->value,
'platform_error_code' => $this->platformErrorCode,
'user_message' => $this->userMessage,
'raw_response' => $this->redactTokens($this->rawResponse),
'raw_response' => $this->rawResponse !== null ? TokenRedactor::redact($this->rawResponse) : null,
];
}
private function redactTokens(?string $text): ?string
{
if ($text === null) {
return null;
}
// Redact common token patterns from API error responses
return preg_replace(
[
'/access_token=([^&"\s]+)/',
'/"access_token"\s*:\s*"([^"]+)"/',
'/Bearer\s+\S+/',
],
[
'access_token=[REDACTED]',
'"access_token":"[REDACTED]"',
'Bearer [REDACTED]',
],
$text
);
}
abstract public static function fromApiResponse(mixed $response): static;
abstract public function platform(): string;

View file

@ -5,6 +5,7 @@
namespace App\Services\Social\Concerns;
use App\Models\PostPlatform;
use App\Services\Social\TokenRedactor;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
@ -38,20 +39,6 @@ protected function socialHttp(): PendingRequest
protected function redactResponseBody(string $body): string
{
return preg_replace(
[
'/access_token=([^&"\s]+)/',
'/"access_token"\s*:\s*"([^"]+)"/',
'/Bearer\s+\S+/',
'/"token"\s*:\s*"([^"]+)"/',
],
[
'access_token=[REDACTED]',
'"access_token":"[REDACTED]"',
'Bearer [REDACTED]',
'"token":"[REDACTED]"',
],
$body
);
return TokenRedactor::redact($body);
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
/**
* Strips OAuth tokens from raw HTTP bodies before they hit logs or
* exception messages. Centralizes the regex patterns so they evolve in
* one place adding a new token format (e.g. provider-specific) means
* extending this list, not hunting through the codebase.
*/
class TokenRedactor
{
public static function redact(string $body): string
{
return preg_replace(
[
'/access_token=([^&"\s]+)/',
'/"access_token"\s*:\s*"([^"]+)"/',
'/Bearer\s+\S+/',
'/"token"\s*:\s*"([^"]+)"/',
],
[
'access_token=[REDACTED]',
'"access_token":"[REDACTED]"',
'Bearer [REDACTED]',
'"token":"[REDACTED]"',
],
$body
);
}
}

View file

@ -58,7 +58,7 @@ public function send(Closure $request): Response
if ($response->failed()) {
Log::error("TokenRefreshClient: {$name} token refresh failed", [
'body' => $this->redactBody($response->body()),
'body' => TokenRedactor::redact($response->body()),
]);
$body = $response->json();
@ -71,23 +71,4 @@ public function send(Closure $request): Response
return $response;
}
private function redactBody(string $body): string
{
return preg_replace(
[
'/access_token=([^&"\s]+)/',
'/"access_token"\s*:\s*"([^"]+)"/',
'/Bearer\s+\S+/',
'/"token"\s*:\s*"([^"]+)"/',
],
[
'access_token=[REDACTED]',
'"access_token":"[REDACTED]"',
'Bearer [REDACTED]',
'"token":"[REDACTED]"',
],
$body
);
}
}