Add timeout, retry and error handling to remote auth HTTP calls

RemoteAuthService::getVerifyCredentials, getFollowing and getToken made
outbound HTTP requests to a user-controlled remote instance during the
Mastodon login flow with no timeout, no retry and no exception handling.
A slow or hostile instance could hang the request or surface an uncaught
exception.

Wrap all three in timeout(20)->retry(3, 750) with try/catch that returns
false on failure, matching the existing pattern in isDomainCompatible().
Callers already treat a falsy return as a failure; add the missing guard
at the one verify_credentials call site that accessed the result array
without checking it first.

Adds RemoteAuthServiceTest covering connection failure, server error and
success paths.
pull/7036/head
Your Name 3 weeks ago
parent 7b7392dcaf
commit 2ad6e28318

@ -326,6 +326,7 @@ class RemoteAuthController extends Controller
$token = $request->session()->get('oauth_remote_session_token');
$res = RemoteAuthService::getVerifyCredentials($domain, $token);
abort_if(! $res || ! isset($res['acct']), 403, 'Invalid credentials');
$res['_webfinger'] = strtolower('@'.$res['acct'].'@'.$domain);
$res['_domain'] = strtolower($domain);
$request->session()->put('oauth_remasto_id', $res['id']);

@ -71,14 +71,23 @@ class RemoteAuthService
}
$url = 'https://'.$domain.'/oauth/token';
$res = Http::asForm()->post($url, [
'code' => $code,
'grant_type' => 'authorization_code',
'client_id' => $raw->client_id,
'client_secret' => $raw->client_secret,
'redirect_uri' => $raw->redirect_uri,
'scope' => 'read',
]);
try {
$res = Http::asForm()->timeout(20)->retry(3, 750)->post($url, [
'code' => $code,
'grant_type' => 'authorization_code',
'client_id' => $raw->client_id,
'client_secret' => $raw->client_secret,
'redirect_uri' => $raw->redirect_uri,
'scope' => 'read',
]);
} catch (RequestException $e) {
return false;
} catch (ConnectionException $e) {
return false;
} catch (\Exception $e) {
return false;
}
return $res;
}
@ -92,7 +101,18 @@ class RemoteAuthService
$url = 'https://'.$domain.'/api/v1/accounts/verify_credentials';
$res = Http::withToken($code)->get($url);
try {
$res = Http::withToken($code)->timeout(20)->retry(3, 750)->get($url);
if (! $res->ok()) {
return false;
}
} catch (RequestException $e) {
return false;
} catch (ConnectionException $e) {
return false;
} catch (\Exception $e) {
return false;
}
return $res->json();
}
@ -108,7 +128,18 @@ class RemoteAuthService
$key = self::CACHE_KEY.'get-following:code:'.substr($code, 0, 16).substr($code, -5).':domain:'.$domain.':id:'.$id;
return Cache::remember($key, 3600, function () use ($url, $code) {
$res = Http::withToken($code)->get($url);
try {
$res = Http::withToken($code)->timeout(20)->retry(3, 750)->get($url);
if (! $res->ok()) {
return false;
}
} catch (RequestException $e) {
return false;
} catch (ConnectionException $e) {
return false;
} catch (\Exception $e) {
return false;
}
return $res->json();
});

@ -0,0 +1,99 @@
<?php
namespace Tests\Feature;
use App\Models\RemoteAuthInstance;
use App\Services\Account\RemoteAuthService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class RemoteAuthServiceTest extends TestCase
{
use RefreshDatabase;
private function activeInstance(string $domain = 'mastodon.example'): RemoteAuthInstance
{
return RemoteAuthInstance::create([
'domain' => $domain,
'client_id' => 'cid',
'client_secret' => 'secret',
'redirect_uri' => url('/auth/mastodon/callback'),
'active' => true,
'banned' => false,
]);
}
#[Test]
public function verify_credentials_returns_false_on_connection_failure()
{
$this->activeInstance();
Http::fake(function () {
throw new ConnectionException('timed out');
});
$res = RemoteAuthService::getVerifyCredentials('mastodon.example', 'token');
$this->assertFalse($res);
}
#[Test]
public function verify_credentials_returns_false_on_server_error()
{
$this->activeInstance();
Http::fake([
'*' => Http::response('nope', 500),
]);
$res = RemoteAuthService::getVerifyCredentials('mastodon.example', 'token');
$this->assertFalse($res);
}
#[Test]
public function verify_credentials_returns_json_on_success()
{
$this->activeInstance();
Http::fake([
'*' => Http::response(['acct' => 'alice', 'id' => '1'], 200),
]);
$res = RemoteAuthService::getVerifyCredentials('mastodon.example', 'token');
$this->assertIsArray($res);
$this->assertSame('alice', $res['acct']);
}
#[Test]
public function get_following_returns_false_on_connection_failure()
{
$this->activeInstance();
Http::fake(function () {
throw new ConnectionException('timed out');
});
$res = RemoteAuthService::getFollowing('mastodon.example', 'token', 42);
$this->assertFalse($res);
}
#[Test]
public function get_token_returns_false_on_connection_failure()
{
$this->activeInstance();
Http::fake(function () {
throw new ConnectionException('timed out');
});
$res = RemoteAuthService::getToken('mastodon.example', 'code');
$this->assertFalse($res);
}
}
Loading…
Cancel
Save