From d729c34c0e787b13b80f5580a4206a62021fc20c Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 14 Jun 2026 09:18:24 -0300 Subject: [PATCH] Harden Telegram connect: one-time code, drop session, decode validation; switch icon to png; widen add-social dialog --- .../Controllers/Auth/TelegramController.php | 11 ++---- .../Webhooks/TelegramWebhookController.php | 6 +++ app/Services/Social/TelegramConnectCode.php | 11 ++++-- public/images/accounts/telegram.png | Bin 0 -> 15821 bytes public/images/accounts/telegram.svg | 10 ----- .../components/accounts/AddSocialDialog.vue | 6 +-- .../accounts/TelegramConnectDialog.vue | 6 ++- resources/js/composables/usePlatformLogo.ts | 8 +++- .../Feature/Social/TelegramConnectionTest.php | 37 +++++++++++++----- 9 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 public/images/accounts/telegram.png delete mode 100644 public/images/accounts/telegram.svg diff --git a/app/Http/Controllers/Auth/TelegramController.php b/app/Http/Controllers/Auth/TelegramController.php index e7fd8202..bf84f7e3 100644 --- a/app/Http/Controllers/Auth/TelegramController.php +++ b/app/Http/Controllers/Auth/TelegramController.php @@ -15,8 +15,6 @@ class TelegramController extends SocialController { protected SocialPlatform $platform = SocialPlatform::Telegram; - private const SESSION_KEY = 'telegram_connect_code'; - /** * Start a connection: issue a signed one-off code the user posts in their * channel (`/connect `). The code carries the workspace, so the webhook @@ -33,26 +31,23 @@ public function connect(Request $request): JsonResponse $this->ensureSocialAccountLimit($workspace); $expiresAt = now()->addMinutes(15); - $code = TelegramConnectCode::issue($workspace->id, $expiresAt); - - $request->session()->put(self::SESSION_KEY, $code); return response()->json([ - 'code' => $code, + 'code' => TelegramConnectCode::issue($workspace->id, $expiresAt), 'bot_username' => config('trypost.platforms.telegram.bot_username'), 'expires_at' => $expiresAt->toIso8601String(), ]); } /** - * Poll whether the channel issued in this session has been linked yet. + * Poll whether the channel for the given code has been linked yet. */ public function status(Request $request): JsonResponse { $workspace = $request->user()->currentWorkspace; abort_if($workspace === null, SymfonyResponse::HTTP_CONFLICT, 'No active workspace.'); - $payload = TelegramConnectCode::decode($request->session()->get(self::SESSION_KEY)); + $payload = TelegramConnectCode::decode($request->query('code')); if ($payload === null) { return response()->json(['status' => TelegramConnectStatus::Unknown->value]); diff --git a/app/Http/Controllers/Webhooks/TelegramWebhookController.php b/app/Http/Controllers/Webhooks/TelegramWebhookController.php index 4d673c45..6aa0e05f 100644 --- a/app/Http/Controllers/Webhooks/TelegramWebhookController.php +++ b/app/Http/Controllers/Webhooks/TelegramWebhookController.php @@ -12,6 +12,7 @@ use App\Services\Social\TelegramConnectCode; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Support\Facades\Cache; use Laravel\Pennant\Feature; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; @@ -59,6 +60,11 @@ public function handle(Request $request): Response return response()->noContent(); } + // Consume the code once so a leaked code can't be replayed to link another chat. + if (! Cache::add("telegram:connect:{$payload['nonce']}", true, now()->addMinutes(15))) { + return response()->noContent(); + } + $workspace->socialAccounts()->updateOrCreate( [ 'platform' => SocialPlatform::Telegram->value, diff --git a/app/Services/Social/TelegramConnectCode.php b/app/Services/Social/TelegramConnectCode.php index 97cb45f5..fe34e23b 100644 --- a/app/Services/Social/TelegramConnectCode.php +++ b/app/Services/Social/TelegramConnectCode.php @@ -28,11 +28,11 @@ public static function issue(string $workspaceId, CarbonInterface $expiresAt): s /** * Decode and validate a code, returning its payload or null when the code is - * missing, tampered with, or expired. + * missing, tampered with, malformed, or expired. * * @return array{workspace_id: string, nonce: string, expires_at: int}|null */ - public static function decode(?string $code): ?array + public static function decode(mixed $code): ?array { if (! is_string($code) || $code === '') { return null; @@ -44,7 +44,12 @@ public static function decode(?string $code): ?array return null; } - if (! is_array($payload) || now()->getTimestamp() > (int) data_get($payload, 'expires_at')) { + if ( + ! is_array($payload) + || ! is_string(data_get($payload, 'workspace_id')) + || ! is_string(data_get($payload, 'nonce')) + || now()->getTimestamp() > (int) data_get($payload, 'expires_at') + ) { return null; } diff --git a/public/images/accounts/telegram.png b/public/images/accounts/telegram.png new file mode 100644 index 0000000000000000000000000000000000000000..80ebeb22c6912b13fba089ecc68eb82975a6dbbe GIT binary patch literal 15821 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 AD*ylh literal 0 HcmV?d00001 diff --git a/public/images/accounts/telegram.svg b/public/images/accounts/telegram.svg deleted file mode 100644 index 8e5fccef..00000000 --- a/public/images/accounts/telegram.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/js/components/accounts/AddSocialDialog.vue b/resources/js/components/accounts/AddSocialDialog.vue index bb754109..06a57081 100644 --- a/resources/js/components/accounts/AddSocialDialog.vue +++ b/resources/js/components/accounts/AddSocialDialog.vue @@ -101,7 +101,7 @@ const platformTheme: Record< telegram: { bg: 'bg-sky-200', rotate: '-rotate-2', - image: '/images/accounts/telegram.svg', + image: '/images/accounts/telegram.png', }, }; @@ -130,7 +130,7 @@ const connectPlatform = (platformValue: string) => {