Fix #118 Add a throttling system for the AccountCreationToken push notification endpoint

This commit is contained in:
Timothée Jaussoin 2023-09-06 14:58:21 +02:00
parent 03bd8d8114
commit 1debbc5f10
7 changed files with 89 additions and 18 deletions

View file

@ -10,6 +10,7 @@ APP_FLEXISIP_PUSHER_PATH=
APP_FLEXISIP_PUSHER_FIREBASE_KEY=
APP_API_KEY_EXPIRATION_MINUTES=60 # Number of minutes the generated API Keys are valid
APP_API_ACCOUNT_CREATION_TOKEN_RETRY_MINUTES=60 # Number of minutes between two consecutive account_creation_token creation
# Risky toggles
APP_ADMINS_MANAGE_MULTI_DOMAINS=false # Allow admins to handle all the accounts in the database

View file

@ -19,15 +19,16 @@
namespace App\Http\Controllers\Api\Account;
use App\AccountCreationRequestToken;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
use App\AccountCreationToken;
use App\Libraries\FlexisipPusherConnector;
use App\AccountCreationRequestToken;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Libraries\FlexisipPusherConnector;
use App\Rules\AccountCreationRequestToken as RulesAccountCreationRequestToken;
class CreationTokenController extends Controller
@ -40,6 +41,17 @@ class CreationTokenController extends Controller
'pn_prid' => 'required',
]);
$last = AccountCreationToken::where('pn_provider', $request->get('pn_provider'))
->where('pn_paparam', $request->get('pn_param'))
->where('pn_prid', $request->get('pn_prid'))
->where('created_at', '>=', Carbon::now()->subMinutes(config('app.account_creation_token_retry_minutes'))->toDateTimeString())
->latest()
->first();
if ($last) {
abort(429, 'Last token requested too recently');
}
$token = new AccountCreationToken;
$token->token = Str::random(WebAuthenticateController::$emailCodeSize);
$token->pn_provider = $request->get('pn_provider');

View file

@ -38,6 +38,11 @@ return [
*/
'api_key_expiration_minutes' => env('APP_API_KEY_EXPIRATION_MINUTES', 60),
/**
* Amount of minutes before re-authorizing the generation of a new account creation token
*/
'account_creation_token_retry_minutes' => env('APP_API_ACCOUNT_CREATION_TOKEN_RETRY_MINUTES', 60),
/**
* External interfaces
*/

View file

@ -24,6 +24,7 @@ use Illuminate\Support\Str;
use App\AccountCreationToken;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use Illuminate\Support\Carbon;
class AccountCreationTokenFactory extends Factory
{
@ -36,7 +37,8 @@ class AccountCreationTokenFactory extends Factory
'pn_param' => $this->faker->uuid,
'pn_prid' => $this->faker->uuid,
'token' => Str::random(WebAuthenticateController::$emailCodeSize),
'used' => false
'used' => false,
'created_at' => Carbon::now()
];
}
}

View file

@ -43,20 +43,42 @@ class ApiAccountCreationTokenTest extends TestCase
protected $pnParam = 'param';
protected $pnPrid = 'id';
public function testMandatoryParameters()
{
$response = $this->json($this->method, $this->tokenRoute);
$response->assertStatus(422);
}
public function testCorrectParameters()
{
$response = $this->json($this->method, $this->tokenRoute, [
$this->assertSame(AccountCreationToken::count(), 0);
$this->json($this->method, $this->tokenRoute, [
'pn_provider' => $this->pnProvider,
'pn_param' => $this->pnParam,
'pn_prid' => $this->pnPrid,
]);
$response->assertStatus(503);
])->assertStatus(503);
}
public function testMandatoryParameters()
{
$this->json($this->method, $this->tokenRoute)->assertStatus(422);
$this->json($this->method, $this->tokenRoute, [
'pn_provider' => null,
'pn_param' => null,
'pn_prid' => null,
])->assertStatus(422);
}
public function testExpiration()
{
$existing = AccountCreationToken::factory()->create();
$this->json($this->method, $this->tokenRoute, [
'pn_provider' => $this->pnProvider,
'pn_param' => $this->pnParam,
'pn_prid' => $this->pnPrid,
])->assertStatus(503);
$this->json($this->method, $this->tokenRoute, [
'pn_provider' => $existing->pnProvider,
'pn_param' => $existing->pnParam,
'pn_prid' => $existing->pnPrid,
])->assertStatus(422);
}
public function testAdminEndpoint()

View file

@ -540,6 +540,32 @@ class ApiAccountTest extends TestCase
->assertJsonValidationErrors(['email']);
}
public function testNonAsciiPasswordAdmin()
{
$admin = Admin::factory()->create();
$admin->account->generateApiKey();
$admin->account->save();
$username = 'username';
$response = $this->generateFirstResponse($admin->account->passwords()->first(), $this->method, $this->route);
$this->generateSecondResponse($admin->account->passwords()->first(), $response)
->json($this->method, $this->route, [
'username' => $username,
'email' => 'email@test.com',
'domain' => 'server.com',
'algorithm' => 'SHA-256',
'password' => 'nonascii€',
])
->assertStatus(200);
$password = Account::where('username', $username)->first()->passwords()->first();
$response = $this->generateFirstResponse($password, 'GET', '/api/accounts/me');
$response = $this->generateSecondResponse($password, $response)
->json('GET', '/api/accounts/me');
}
public function testEditAdmin()
{
$password = Password::factory()->create();
@ -554,18 +580,18 @@ class ApiAccountTest extends TestCase
$password = 'other';
$this->keyAuthenticated($admin->account)
->json('PUT', $this->route. '/1234')
->json('PUT', $this->route . '/1234')
->assertStatus(422)
->assertJsonValidationErrors(['username']);
$this->keyAuthenticated($admin->account)
->json('PUT', $this->route. '/1234', [
->json('PUT', $this->route . '/1234', [
'username' => 'good'
])
->assertStatus(422);
$this->keyAuthenticated($admin->account)
->json('PUT', $this->route. '/'. $account->id, [
->json('PUT', $this->route . '/' . $account->id, [
'username' => $username,
'algorithm' => $algorithm,
'password' => $password,

View file

@ -30,6 +30,9 @@ abstract class TestCase extends BaseTestCase
const ALGORITHMS = ['md5' => 'MD5', 'sha256' => 'SHA-256'];
protected $route = '/api/accounts/me';
protected $method = 'GET';
protected function keyAuthenticated(Account $account)
{
return $this->withHeaders([
@ -37,11 +40,11 @@ abstract class TestCase extends BaseTestCase
]);
}
protected function generateFirstResponse(Password $password)
protected function generateFirstResponse(Password $password, ?string $method = null, ?string $route = null)
{
return $this->withHeaders([
'From' => 'sip:'.$password->account->identifier
])->json($this->method, $this->route);
])->json($method ?? $this->method, $route ?? $this->route);
}
protected function generateSecondResponse(Password $password, $firstResponse)