diff --git a/flexiapi/.env.example b/flexiapi/.env.example index c318134..4e8770e 100644 --- a/flexiapi/.env.example +++ b/flexiapi/.env.example @@ -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 diff --git a/flexiapi/app/Http/Controllers/Api/Account/CreationTokenController.php b/flexiapi/app/Http/Controllers/Api/Account/CreationTokenController.php index c893781..92f8850 100644 --- a/flexiapi/app/Http/Controllers/Api/Account/CreationTokenController.php +++ b/flexiapi/app/Http/Controllers/Api/Account/CreationTokenController.php @@ -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'); diff --git a/flexiapi/config/app.php b/flexiapi/config/app.php index 8f4f023..c56dae2 100644 --- a/flexiapi/config/app.php +++ b/flexiapi/config/app.php @@ -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 */ diff --git a/flexiapi/database/factories/AccountCreationTokenFactory.php b/flexiapi/database/factories/AccountCreationTokenFactory.php index d02f5ff..bcfd3af 100644 --- a/flexiapi/database/factories/AccountCreationTokenFactory.php +++ b/flexiapi/database/factories/AccountCreationTokenFactory.php @@ -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() ]; } } diff --git a/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php b/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php index cbac38b..f90ca7f 100644 --- a/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php +++ b/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php @@ -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() diff --git a/flexiapi/tests/Feature/ApiAccountTest.php b/flexiapi/tests/Feature/ApiAccountTest.php index 2e0bf84..2d8b249 100644 --- a/flexiapi/tests/Feature/ApiAccountTest.php +++ b/flexiapi/tests/Feature/ApiAccountTest.php @@ -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, diff --git a/flexiapi/tests/TestCase.php b/flexiapi/tests/TestCase.php index f7d4e56..4e1076f 100644 --- a/flexiapi/tests/TestCase.php +++ b/flexiapi/tests/TestCase.php @@ -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)