From f6c5562201ecb97be33f5dffe3f713d0b6de872d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Thu, 11 Jul 2024 12:51:02 +0000 Subject: [PATCH] Fix FLEXIAPI-192 Add DotEnv configuration to allow the expiration of tokens and codes in the app --- CHANGELOG.md | 4 ++ flexiapi/.env.example | 12 +++- flexiapi/app/Account.php | 11 +++- flexiapi/app/AccountCreationToken.php | 2 + .../ClearAccountsTombstones.php} | 4 +- .../Commands/{ => Accounts}/ClearApiKeys.php | 7 ++- .../ClearUnconfirmed.php} | 4 +- .../{ => Accounts}/CreateAdminAccount.php | 2 +- .../CreateAdminTest.php} | 5 +- .../Seed.php} | 4 +- .../SetAdmin.php} | 4 +- .../Commands/{ => Digest}/ClearNonces.php | 2 +- .../CreateUpdate.php} | 4 +- flexiapi/app/Consommable.php | 28 +++++++++ flexiapi/app/EmailChangeCode.php | 1 + .../Account/ProvisioningController.php | 6 +- .../Account/RecoveryController.php | 8 +++ .../Api/Account/AccountController.php | 8 ++- .../Account/Create/Api/AsAdminRequest.php | 18 +++++- .../Requests/Account/Create/Api/Request.php | 20 ++++++- .../Http/Requests/Account/Create/Request.php | 17 ++++++ .../Account/Create/Web/AsAdminRequest.php | 17 ++++++ .../Requests/Account/Create/Web/Request.php | 17 ++++++ .../Account/Update/Api/AsAdminRequest.php | 17 ++++++ .../Http/Requests/Account/Update/Request.php | 17 ++++++ .../Account/Update/Web/AsAdminRequest.php | 17 ++++++ flexiapi/app/Http/Requests/Api.php | 17 ++++++ flexiapi/app/Http/Requests/AsAdmin.php | 17 ++++++ flexiapi/app/Mail/RecoverByCode.php | 1 + flexiapi/app/PhoneChangeCode.php | 1 + flexiapi/app/ProvisioningToken.php | 1 + flexiapi/app/RecoveryCode.php | 2 + .../Rules/AccountCreationTokenNotExpired.php | 38 +++++++++++++ flexiapi/app/Services/AccountService.php | 18 +++++- flexiapi/config/app.php | 6 +- flexiapi/config/rcfile | 5 ++ .../factories/AccountCreationTokenFactory.php | 7 +++ .../views/account/recovery/show.blade.php | 14 ++++- flexiapi/resources/views/errors/410.blade.php | 5 ++ .../resources/views/errors/minimal.blade.php | 2 +- .../views/mails/authentication.blade.php | 5 ++ .../views/mails/authentication_text.blade.php | 4 ++ .../tests/Feature/AccountProvisioningTest.php | 50 +++++++++++----- .../Feature/ApiAccountCreationTokenTest.php | 57 ++++++++++++------- .../Feature/ApiAccountEmailChangeTest.php | 23 ++++++++ .../Feature/ApiAccountPhoneChangeTest.php | 23 ++++++++ 46 files changed, 486 insertions(+), 66 deletions(-) rename flexiapi/app/Console/Commands/{ClearOldAccountsTombstones.php => Accounts/ClearAccountsTombstones.php} (94%) rename flexiapi/app/Console/Commands/{ => Accounts}/ClearApiKeys.php (90%) rename flexiapi/app/Console/Commands/{RemoveUnconfirmedAccounts.php => Accounts/ClearUnconfirmed.php} (95%) rename flexiapi/app/Console/Commands/{ => Accounts}/CreateAdminAccount.php (98%) rename flexiapi/app/Console/Commands/{CreateAdminAccountTest.php => Accounts/CreateAdminTest.php} (95%) rename flexiapi/app/Console/Commands/{RunAccountSeeder.php => Accounts/Seed.php} (96%) rename flexiapi/app/Console/Commands/{SetAccountAdmin.php => Accounts/SetAdmin.php} (95%) rename flexiapi/app/Console/Commands/{ => Digest}/ClearNonces.php (97%) rename flexiapi/app/Console/Commands/{CreateSipDomain.php => SipDomains/CreateUpdate.php} (95%) create mode 100644 flexiapi/app/Rules/AccountCreationTokenNotExpired.php create mode 100644 flexiapi/config/rcfile create mode 100644 flexiapi/resources/views/errors/410.blade.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 083683b..c589e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Flexisip Account Manager Changelog +v1.6 +---- +- Fix FLEXIAPI-192 Add DotEnv configuration to allow the expiration of tokens and codes in the app + v1.5 --- - Fix FLEXIAPI-195 Fix LiblinphoneTesterAccoutSeeder to fit with the latest Account related changes diff --git a/flexiapi/.env.example b/flexiapi/.env.example index 379d983..4d7ab4d 100644 --- a/flexiapi/.env.example +++ b/flexiapi/.env.example @@ -9,7 +9,6 @@ APP_LINPHONE_DAEMON_UNIX_PATH= APP_FLEXISIP_PUSHER_PATH= APP_FLEXISIP_PUSHER_FIREBASE_KEYSMAP= # Each pair is separated using a space and defined as a key:value -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 APP_ALLOW_PHONE_NUMBER_USERNAME_ADMIN_API=false # Allow phone numbers to be set as username in admin account creation endpoints @@ -22,9 +21,16 @@ ACCOUNT_PROXY_REGISTRAR_ADDRESS=sip.example.com # Proxy registrar address, can b ACCOUNT_TRANSPORT_PROTOCOL_TEXT="TLS (recommended), TCP or UDP" # Simple text, to explain how the SIP server can be reached ACCOUNT_REALM=null # Default realm for the accounts, fallback to the domain if not set, enforce null by default +# Expiration time for tokens and code, in minutes, 0 means no expiration +APP_ACCOUNT_CREATION_TOKEN_EXPIRATION_MINUTES=0 +APP_EMAIL_CHANGE_CODE_EXPIRATION_MINUTES=10 +APP_PHONE_CHANGE_CODE_EXPIRATION_MINUTES=10 +APP_RECOVERY_CODE_EXPIRATION_MINUTES=10 +APP_PROVISIONING_TOKEN_EXPIRATION_MINUTES=0 +APP_API_KEY_EXPIRATION_MINUTES=60 # Number of minutes the unused API Keys are valid + # Account creation ACCOUNT_EMAIL_UNIQUE=false # Emails are unique between all the accounts -ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE=false ACCOUNT_BLACKLISTED_USERNAMES= ACCOUNT_USERNAME_REGEX="^[a-z0-9+_.-]*$" ACCOUNT_DEFAULT_PASSWORD_ALGORITHM=SHA-256 # Can ONLY be MD5 or SHA-256 in capital, default to SHA-256 @@ -45,12 +51,12 @@ INSTANCE_CUSTOM_THEME=false INSTANCE_CONFIRMED_REGISTRATION_TEXT= # Markdown text displayed when an account is confirmed WEB_PANEL=true # Fully enable/disable the web panels -NEWSLETTER_REGISTRATION_ADDRESS= # Address to contact when a user wants to register to the newsletter PUBLIC_REGISTRATION=true # Toggle to enable/disable the public registration forms PHONE_AUTHENTICATION=true # Toggle to enable/disable the SMS support, requires public registration DEVICES_MANAGEMENT=false # Toggle to enable/disable the devices management supporttrue INTERCOM_FEATURES=false # Toggle to enable/disable the intercom related features +NEWSLETTER_REGISTRATION_ADDRESS= # Address to contact when a user wants to register to the newsletter TERMS_OF_USE_URL= # A URL pointing to the Terms of Use PRIVACY_POLICY_URL= # A URL pointing to the Privacy Policy APP_PROJECT_URL= # A URL pointing to the project information page diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index 9832b04..d3bfcbd 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -38,7 +38,7 @@ class Account extends Authenticatable protected $with = ['passwords', 'activationExpiration', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries']; protected $hidden = ['expire_time', 'confirmation_key', 'pivot', 'currentProvisioningToken', 'currentRecoveryCode', 'dictionaryEntries']; - protected $appends = ['realm', 'confirmation_key_expires', 'provisioning_token', 'dictionary']; + protected $appends = ['realm', 'confirmation_key_expires', 'provisioning_token', 'provisioning_token_expire_at', 'dictionary']; protected $casts = [ 'activated' => 'boolean', ]; @@ -272,6 +272,15 @@ class Account extends Authenticatable return null; } + public function getProvisioningTokenExpireAtAttribute(): ?string + { + if ($this->currentProvisioningToken) { + return $this->currentProvisioningToken->expire_at; + } + + return null; + } + public function getIdentifierAttribute(): string { return $this->attributes['username'] . '@' . $this->attributes['domain']; diff --git a/flexiapi/app/AccountCreationToken.php b/flexiapi/app/AccountCreationToken.php index 1105830..cafaa94 100644 --- a/flexiapi/app/AccountCreationToken.php +++ b/flexiapi/app/AccountCreationToken.php @@ -26,6 +26,8 @@ class AccountCreationToken extends Consommable use HasFactory; protected $hidden = ['id', 'updated_at', 'created_at']; + protected $appends = ['expire_at']; + protected ?string $configExpirationMinutesKey = 'account_creation_token_expiration_minutes'; public function accountCreationRequestToken() { diff --git a/flexiapi/app/Console/Commands/ClearOldAccountsTombstones.php b/flexiapi/app/Console/Commands/Accounts/ClearAccountsTombstones.php similarity index 94% rename from flexiapi/app/Console/Commands/ClearOldAccountsTombstones.php rename to flexiapi/app/Console/Commands/Accounts/ClearAccountsTombstones.php index 83e2e95..9deac53 100644 --- a/flexiapi/app/Console/Commands/ClearOldAccountsTombstones.php +++ b/flexiapi/app/Console/Commands/Accounts/ClearAccountsTombstones.php @@ -17,14 +17,14 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\Accounts; use Illuminate\Console\Command; use Carbon\Carbon; use App\AccountTombstone; -class ClearOldAccountsTombstones extends Command +class ClearAccountsTombstones extends Command { protected $signature = 'accounts:clear-accounts-tombstones {days} {--apply}'; protected $description = 'Clear deleted accounts tombstones after n days'; diff --git a/flexiapi/app/Console/Commands/ClearApiKeys.php b/flexiapi/app/Console/Commands/Accounts/ClearApiKeys.php similarity index 90% rename from flexiapi/app/Console/Commands/ClearApiKeys.php rename to flexiapi/app/Console/Commands/Accounts/ClearApiKeys.php index cb83dfd..e7ef164 100644 --- a/flexiapi/app/Console/Commands/ClearApiKeys.php +++ b/flexiapi/app/Console/Commands/Accounts/ClearApiKeys.php @@ -17,7 +17,7 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\Accounts; use Illuminate\Console\Command; use Carbon\Carbon; @@ -38,6 +38,11 @@ class ClearApiKeys extends Command { $minutes = $this->argument('minutes') ?? config('app.api_key_expiration_minutes'); + if ($minutes == 0) { + $this->info('Expiration time is set to 0, nothing to clear'); + return 0; + } + $this->info('Deleting api keys unused after ' . $minutes . ' minutes'); $count = ApiKey::where( diff --git a/flexiapi/app/Console/Commands/RemoveUnconfirmedAccounts.php b/flexiapi/app/Console/Commands/Accounts/ClearUnconfirmed.php similarity index 95% rename from flexiapi/app/Console/Commands/RemoveUnconfirmedAccounts.php rename to flexiapi/app/Console/Commands/Accounts/ClearUnconfirmed.php index a6c7f15..a362934 100644 --- a/flexiapi/app/Console/Commands/RemoveUnconfirmedAccounts.php +++ b/flexiapi/app/Console/Commands/Accounts/ClearUnconfirmed.php @@ -17,14 +17,14 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\Accounts; use Illuminate\Console\Command; use Carbon\Carbon; use App\Account; -class RemoveUnconfirmedAccounts extends Command +class ClearUnconfirmed extends Command { protected $signature = 'accounts:clear-unconfirmed {days} {--apply} {--and-confirmed}'; protected $description = 'Clear unconfirmed accounts after n days'; diff --git a/flexiapi/app/Console/Commands/CreateAdminAccount.php b/flexiapi/app/Console/Commands/Accounts/CreateAdminAccount.php similarity index 98% rename from flexiapi/app/Console/Commands/CreateAdminAccount.php rename to flexiapi/app/Console/Commands/Accounts/CreateAdminAccount.php index edb88ee..98335f2 100644 --- a/flexiapi/app/Console/Commands/CreateAdminAccount.php +++ b/flexiapi/app/Console/Commands/Accounts/CreateAdminAccount.php @@ -17,7 +17,7 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\Accounts; use Illuminate\Console\Command; use Carbon\Carbon; diff --git a/flexiapi/app/Console/Commands/CreateAdminAccountTest.php b/flexiapi/app/Console/Commands/Accounts/CreateAdminTest.php similarity index 95% rename from flexiapi/app/Console/Commands/CreateAdminAccountTest.php rename to flexiapi/app/Console/Commands/Accounts/CreateAdminTest.php index dc02764..cfd4921 100644 --- a/flexiapi/app/Console/Commands/CreateAdminAccountTest.php +++ b/flexiapi/app/Console/Commands/Accounts/CreateAdminTest.php @@ -17,16 +17,15 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\Accounts; use Illuminate\Console\Command; use Carbon\Carbon; use App\Account; use App\ApiKey; -use App\SipDomain; -class CreateAdminAccountTest extends Command +class CreateAdminTest extends Command { protected $signature = 'accounts:create-admin-test'; protected $description = 'Create a test admin account, only for tests purpose'; diff --git a/flexiapi/app/Console/Commands/RunAccountSeeder.php b/flexiapi/app/Console/Commands/Accounts/Seed.php similarity index 96% rename from flexiapi/app/Console/Commands/RunAccountSeeder.php rename to flexiapi/app/Console/Commands/Accounts/Seed.php index de512e9..f54cbf6 100644 --- a/flexiapi/app/Console/Commands/RunAccountSeeder.php +++ b/flexiapi/app/Console/Commands/Accounts/Seed.php @@ -17,13 +17,13 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\Accounts; use Database\Seeders\LiblinphoneTesterAccoutSeeder; use Illuminate\Console\Command; use Illuminate\Support\Facades\App; -class RunAccountSeeder extends Command +class Seed extends Command { protected $signature = 'accounts:seed {json-file-path}'; protected $description = 'Seed some accounts from a JSON file'; diff --git a/flexiapi/app/Console/Commands/SetAccountAdmin.php b/flexiapi/app/Console/Commands/Accounts/SetAdmin.php similarity index 95% rename from flexiapi/app/Console/Commands/SetAccountAdmin.php rename to flexiapi/app/Console/Commands/Accounts/SetAdmin.php index 306da26..cb6671d 100644 --- a/flexiapi/app/Console/Commands/SetAccountAdmin.php +++ b/flexiapi/app/Console/Commands/Accounts/SetAdmin.php @@ -17,13 +17,13 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\Accounts; use Illuminate\Console\Command; use App\Account; -class SetAccountAdmin extends Command +class SetAdmin extends Command { protected $signature = 'accounts:set-admin {id}'; protected $description = 'Give the admin role to an account'; diff --git a/flexiapi/app/Console/Commands/ClearNonces.php b/flexiapi/app/Console/Commands/Digest/ClearNonces.php similarity index 97% rename from flexiapi/app/Console/Commands/ClearNonces.php rename to flexiapi/app/Console/Commands/Digest/ClearNonces.php index 02e7fb7..8532902 100644 --- a/flexiapi/app/Console/Commands/ClearNonces.php +++ b/flexiapi/app/Console/Commands/Digest/ClearNonces.php @@ -17,7 +17,7 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\Digest; use Illuminate\Console\Command; use Carbon\Carbon; diff --git a/flexiapi/app/Console/Commands/CreateSipDomain.php b/flexiapi/app/Console/Commands/SipDomains/CreateUpdate.php similarity index 95% rename from flexiapi/app/Console/Commands/CreateSipDomain.php rename to flexiapi/app/Console/Commands/SipDomains/CreateUpdate.php index 0544c35..13e0883 100644 --- a/flexiapi/app/Console/Commands/CreateSipDomain.php +++ b/flexiapi/app/Console/Commands/SipDomains/CreateUpdate.php @@ -17,12 +17,12 @@ along with this program. If not, see . */ -namespace App\Console\Commands; +namespace App\Console\Commands\SipDomains; use App\SipDomain; use Illuminate\Console\Command; -class CreateSipDomain extends Command +class CreateUpdate extends Command { protected $signature = 'sip_domains:create-update {domain} {--super}'; protected $description = 'Create a SIP Domain'; diff --git a/flexiapi/app/Consommable.php b/flexiapi/app/Consommable.php index 13f9bfe..d52d0b5 100644 --- a/flexiapi/app/Consommable.php +++ b/flexiapi/app/Consommable.php @@ -2,12 +2,18 @@ namespace App; +use Carbon\Carbon; +use DateTime; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; abstract class Consommable extends Model { protected string $consommableAttribute = 'code'; + protected ?string $configExpirationMinutesKey = null; + protected $casts = [ + 'expire_at' => 'datetime' + ]; public function consume() { @@ -25,4 +31,26 @@ abstract class Consommable extends Model { return $this->{$this->consommableAttribute} == null; } + + public function getExpireAtAttribute(): ?string + { + if ($this->isExpirable()) { + return $this->created_at->addMinutes(config('app.' . $this->configExpirationMinutesKey))->toJSON(); + } + + return null; + } + + public function expired(): bool + { + return ($this->isExpirable() + && Carbon::now()->subMinutes(config('app.' . $this->configExpirationMinutesKey))->isAfter($this->created_at)); + } + + private function isExpirable(): bool + { + return $this->configExpirationMinutesKey != null + && config('app.' . $this->configExpirationMinutesKey) != null + && config('app.' . $this->configExpirationMinutesKey) > 0; + } } diff --git a/flexiapi/app/EmailChangeCode.php b/flexiapi/app/EmailChangeCode.php index 198e613..0331340 100644 --- a/flexiapi/app/EmailChangeCode.php +++ b/flexiapi/app/EmailChangeCode.php @@ -25,6 +25,7 @@ class EmailChangeCode extends Consommable { use HasFactory; + protected ?string $configExpirationMinutesKey = 'email_change_code_expiration_minutes'; protected $hidden = ['id', 'account_id', 'code']; public function account() diff --git a/flexiapi/app/Http/Controllers/Account/ProvisioningController.php b/flexiapi/app/Http/Controllers/Account/ProvisioningController.php index cb8b0a1..1b3670e 100644 --- a/flexiapi/app/Http/Controllers/Account/ProvisioningController.php +++ b/flexiapi/app/Http/Controllers/Account/ProvisioningController.php @@ -128,7 +128,11 @@ class ProvisioningController extends Controller ->firstOrFail(); if ($account->activationExpired() || ($provisioningToken != $account->provisioning_token)) { - abort(404); + return abort(404); + } + + if ($account->currentProvisioningToken->expired()) { + return abort(410, 'Expired'); } $account->activated = true; diff --git a/flexiapi/app/Http/Controllers/Account/RecoveryController.php b/flexiapi/app/Http/Controllers/Account/RecoveryController.php index 08497e7..243cfc0 100644 --- a/flexiapi/app/Http/Controllers/Account/RecoveryController.php +++ b/flexiapi/app/Http/Controllers/Account/RecoveryController.php @@ -120,6 +120,14 @@ class RecoveryController extends Controller $account = Account::where('id', Crypt::decryptString($request->get('account_id')))->firstOrFail(); + if ($account->currentRecoveryCode->expired()) { + return redirect()->route($request->get('method') == 'phone' + ? 'account.recovery.show.phone' + : 'account.recovery.show.email')->withErrors([ + 'code' => 'The code is expired' + ]); + } + if ($account->recovery_code != $code) { return redirect()->route($request->get('method') == 'phone' ? 'account.recovery.show.phone' diff --git a/flexiapi/app/Http/Controllers/Api/Account/AccountController.php b/flexiapi/app/Http/Controllers/Api/Account/AccountController.php index 53bc0ee..4f25c14 100644 --- a/flexiapi/app/Http/Controllers/Api/Account/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/Account/AccountController.php @@ -36,6 +36,7 @@ use App\Libraries\OvhSMS; use App\Mail\RegisterConfirmation; use App\Rules\AccountCreationToken as RulesAccountCreationToken; +use App\Rules\AccountCreationTokenNotExpired; use App\Rules\BlacklistedUsername; use App\Rules\NoUppercase; use App\Rules\SIPUsername; @@ -43,7 +44,6 @@ use App\Rules\WithoutSpaces; use App\Rules\PasswordAlgorithm; use App\Services\AccountService; -use App\SipDomain; class AccountController extends Controller { @@ -122,7 +122,8 @@ class AccountController extends Controller ], 'account_creation_token' => [ 'required', - new RulesAccountCreationToken + new RulesAccountCreationToken, + new AccountCreationTokenNotExpired ] ]); @@ -192,7 +193,8 @@ class AccountController extends Controller ], 'account_creation_token' => [ 'required', - new RulesAccountCreationToken + new RulesAccountCreationToken, + new AccountCreationTokenNotExpired ] ]); diff --git a/flexiapi/app/Http/Requests/Account/Create/Api/AsAdminRequest.php b/flexiapi/app/Http/Requests/Account/Create/Api/AsAdminRequest.php index bc09724..ac809b1 100644 --- a/flexiapi/app/Http/Requests/Account/Create/Api/AsAdminRequest.php +++ b/flexiapi/app/Http/Requests/Account/Create/Api/AsAdminRequest.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests\Account\Create\Api; @@ -7,7 +24,6 @@ use App\Http\Requests\Api as RequestsApi; use App\Http\Requests\AsAdmin; use App\Rules\IsNotPhoneNumber; use App\Rules\PasswordAlgorithm; -use App\SipDomain; class AsAdminRequest extends Request { diff --git a/flexiapi/app/Http/Requests/Account/Create/Api/Request.php b/flexiapi/app/Http/Requests/Account/Create/Api/Request.php index c37e90f..f948298 100644 --- a/flexiapi/app/Http/Requests/Account/Create/Api/Request.php +++ b/flexiapi/app/Http/Requests/Account/Create/Api/Request.php @@ -1,10 +1,28 @@ . +*/ namespace App\Http\Requests\Account\Create\Api; use App\Http\Requests\Account\Create\Request as CreateRequest; use App\Http\Requests\Api as RequestsApi; use App\Rules\AccountCreationToken; +use App\Rules\AccountCreationTokenNotExpired; class Request extends CreateRequest { @@ -18,7 +36,7 @@ class Request extends CreateRequest public function rules() { $rules = parent::rules(); - $rules['account_creation_token'] = ['required', new AccountCreationToken()]; + $rules['account_creation_token'] = ['required', new AccountCreationToken, new AccountCreationTokenNotExpired]; return $rules; } diff --git a/flexiapi/app/Http/Requests/Account/Create/Request.php b/flexiapi/app/Http/Requests/Account/Create/Request.php index 67c8c26..e4e3eba 100644 --- a/flexiapi/app/Http/Requests/Account/Create/Request.php +++ b/flexiapi/app/Http/Requests/Account/Create/Request.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests\Account\Create; diff --git a/flexiapi/app/Http/Requests/Account/Create/Web/AsAdminRequest.php b/flexiapi/app/Http/Requests/Account/Create/Web/AsAdminRequest.php index f340b12..b03a3b3 100644 --- a/flexiapi/app/Http/Requests/Account/Create/Web/AsAdminRequest.php +++ b/flexiapi/app/Http/Requests/Account/Create/Web/AsAdminRequest.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests\Account\Create\Web; diff --git a/flexiapi/app/Http/Requests/Account/Create/Web/Request.php b/flexiapi/app/Http/Requests/Account/Create/Web/Request.php index a700ff4..3572eee 100644 --- a/flexiapi/app/Http/Requests/Account/Create/Web/Request.php +++ b/flexiapi/app/Http/Requests/Account/Create/Web/Request.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests\Account\Create\Web; diff --git a/flexiapi/app/Http/Requests/Account/Update/Api/AsAdminRequest.php b/flexiapi/app/Http/Requests/Account/Update/Api/AsAdminRequest.php index fb887e1..2345dc9 100644 --- a/flexiapi/app/Http/Requests/Account/Update/Api/AsAdminRequest.php +++ b/flexiapi/app/Http/Requests/Account/Update/Api/AsAdminRequest.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests\Account\Update\Api; diff --git a/flexiapi/app/Http/Requests/Account/Update/Request.php b/flexiapi/app/Http/Requests/Account/Update/Request.php index 561a1d9..f55c895 100644 --- a/flexiapi/app/Http/Requests/Account/Update/Request.php +++ b/flexiapi/app/Http/Requests/Account/Update/Request.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests\Account\Update; diff --git a/flexiapi/app/Http/Requests/Account/Update/Web/AsAdminRequest.php b/flexiapi/app/Http/Requests/Account/Update/Web/AsAdminRequest.php index 035fc1d..644848f 100644 --- a/flexiapi/app/Http/Requests/Account/Update/Web/AsAdminRequest.php +++ b/flexiapi/app/Http/Requests/Account/Update/Web/AsAdminRequest.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests\Account\Update\Web; diff --git a/flexiapi/app/Http/Requests/Api.php b/flexiapi/app/Http/Requests/Api.php index 55772ed..4cee262 100644 --- a/flexiapi/app/Http/Requests/Api.php +++ b/flexiapi/app/Http/Requests/Api.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests; diff --git a/flexiapi/app/Http/Requests/AsAdmin.php b/flexiapi/app/Http/Requests/AsAdmin.php index 7cbdd92..0d854a3 100644 --- a/flexiapi/app/Http/Requests/AsAdmin.php +++ b/flexiapi/app/Http/Requests/AsAdmin.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Requests; diff --git a/flexiapi/app/Mail/RecoverByCode.php b/flexiapi/app/Mail/RecoverByCode.php index 188ae82..116d2c3 100644 --- a/flexiapi/app/Mail/RecoverByCode.php +++ b/flexiapi/app/Mail/RecoverByCode.php @@ -45,6 +45,7 @@ class RecoverByCode extends Mailable ? 'mails.authentication_text_custom' : 'mails.authentication_text') ->with([ + 'expiration_minutes' => config('app.recovery_code_expiration_minutes'), 'recovery_code' => $this->account->recovery_code, 'provisioning_link' => route('provisioning.provision', [ 'provisioning_token' => $this->account->provisioning_token, diff --git a/flexiapi/app/PhoneChangeCode.php b/flexiapi/app/PhoneChangeCode.php index dec68bf..9d8dc0b 100644 --- a/flexiapi/app/PhoneChangeCode.php +++ b/flexiapi/app/PhoneChangeCode.php @@ -25,6 +25,7 @@ class PhoneChangeCode extends Consommable { use HasFactory; + protected ?string $configExpirationMinutesKey = 'phone_change_code_expiration_minutes'; protected $hidden = ['id', 'account_id', 'code']; public function account() diff --git a/flexiapi/app/ProvisioningToken.php b/flexiapi/app/ProvisioningToken.php index 4e51af7..6c36623 100644 --- a/flexiapi/app/ProvisioningToken.php +++ b/flexiapi/app/ProvisioningToken.php @@ -25,6 +25,7 @@ class ProvisioningToken extends Consommable { use HasFactory; + protected ?string $configExpirationMinutesKey = 'provisioning_token_expiration_minutes'; protected $casts = [ 'used' => 'boolean', ]; diff --git a/flexiapi/app/RecoveryCode.php b/flexiapi/app/RecoveryCode.php index 4a154f3..8f01edc 100644 --- a/flexiapi/app/RecoveryCode.php +++ b/flexiapi/app/RecoveryCode.php @@ -7,4 +7,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; class RecoveryCode extends Consommable { use HasFactory; + + protected ?string $configExpirationMinutesKey = 'recovery_code_expiration_minutes'; } diff --git a/flexiapi/app/Rules/AccountCreationTokenNotExpired.php b/flexiapi/app/Rules/AccountCreationTokenNotExpired.php new file mode 100644 index 0000000..5406574 --- /dev/null +++ b/flexiapi/app/Rules/AccountCreationTokenNotExpired.php @@ -0,0 +1,38 @@ +. +*/ + +namespace App\Rules; + +use App\AccountCreationToken as AppAccountCreationToken; +use Illuminate\Contracts\Validation\Rule; + +class AccountCreationTokenNotExpired implements Rule +{ + public function passes($attribute, $value) + { + $token = AppAccountCreationToken::where('token', $value)->where('used', false)->first(); + + return $token && !$token->expired(); + } + + public function message() + { + return 'The provided token is expired'; + } +} diff --git a/flexiapi/app/Services/AccountService.php b/flexiapi/app/Services/AccountService.php index bc8c723..2d74d79 100644 --- a/flexiapi/app/Services/AccountService.php +++ b/flexiapi/app/Services/AccountService.php @@ -262,6 +262,10 @@ class AccountService $phoneChangeCode = $account->phoneChangeCode()->firstOrFail(); + if ($phoneChangeCode->expired()) { + return abort(410, 'Expired code'); + } + if ($phoneChangeCode->code == $code) { $account->phone = $phoneChangeCode->phone; $account->activated = true; @@ -329,6 +333,11 @@ class AccountService $account = $request->user(); $emailChangeCode = $account->emailChangeCode()->firstOrFail(); + + if ($emailChangeCode->expired()) { + return abort(410, 'Expired code'); + } + if ($emailChangeCode->validate($code)) { $account->email = $emailChangeCode->email; $account->save(); @@ -371,8 +380,14 @@ class AccountService { $account = $this->recoverAccount($account); + $message = 'Your ' . config('app.name') . ' validation code is ' . $account->recovery_code . ' .'; + + if (config('app.recovery_code_expiration_minutes') > 0) { + $message .= 'The code is available for ' . config('app.recovery_code_expiration_minutes') . ' minutes'; + } + $ovhSMS = new OvhSMS(); - $ovhSMS->send($account->phone, 'Your ' . config('app.name') . ' validation code is ' . $account->recovery_code); + $ovhSMS->send($account->phone, $message); Log::channel('events')->info('Account Service: Sending recovery SMS', ['id' => $account->identifier]); @@ -383,7 +398,6 @@ class AccountService { $account->recover(); $account->provision(); - $account->refresh(); return $account; diff --git a/flexiapi/config/app.php b/flexiapi/config/app.php index 187de49..68a613e 100644 --- a/flexiapi/config/app.php +++ b/flexiapi/config/app.php @@ -40,7 +40,11 @@ return [ * Time limit before the API Key and related cookie are expired */ 'api_key_expiration_minutes' => env('APP_API_KEY_EXPIRATION_MINUTES', 60), - + 'account_creation_token_expiration_minutes' => env('APP_ACCOUNT_CREATION_TOKEN_EXPIRATION_MINUTES', 0), + 'email_change_code_expiration_minutes' => env('APP_EMAIL_CHANGE_CODE_EXPIRATION_MINUTES', 10), + 'phone_change_code_expiration_minutes' => env('APP_PHONE_CHANGE_CODE_EXPIRATION_MINUTES', 10), + 'recovery_code_expiration_minutes' => env('APP_RECOVERY_CODE_EXPIRATION_MINUTES', 10), + 'provisioning_token_expiration_minutes' => env('APP_PROVISIONING_TOKEN_EXPIRATION_MINUTES', 0), /** * Amount of minutes before re-authorizing the generation of a new account creation token */ diff --git a/flexiapi/config/rcfile b/flexiapi/config/rcfile new file mode 100644 index 0000000..d76bcd4 --- /dev/null +++ b/flexiapi/config/rcfile @@ -0,0 +1,5 @@ +[auth_info_0] +test=foobar + +[auth_info_1] +blabla=gnap diff --git a/flexiapi/database/factories/AccountCreationTokenFactory.php b/flexiapi/database/factories/AccountCreationTokenFactory.php index 6482685..e5b4308 100644 --- a/flexiapi/database/factories/AccountCreationTokenFactory.php +++ b/flexiapi/database/factories/AccountCreationTokenFactory.php @@ -43,4 +43,11 @@ class AccountCreationTokenFactory extends Factory 'created_at' => Carbon::now() ]; } + + public function expired() + { + return $this->state(fn (array $attributes) => [ + 'created_at' => Carbon::now()->subMinutes(1000) + ]); + } } diff --git a/flexiapi/resources/views/account/recovery/show.blade.php b/flexiapi/resources/views/account/recovery/show.blade.php index 730390e..6fc5326 100644 --- a/flexiapi/resources/views/account/recovery/show.blade.php +++ b/flexiapi/resources/views/account/recovery/show.blade.php @@ -9,7 +9,12 @@ @if ($method == 'email')
-

Enter your email account to recover it.

+

+ Enter your email account to recover it. + @if (config('app.recovery_code_expiration_minutes') > 0) +
The code will be available {{ config('app.recovery_code_expiration_minutes') }} minutes. + @endif +

@include('parts.errors', ['name' => 'code'])
@@ -29,7 +34,12 @@
@endif @elseif($method == 'phone') -

Enter your phone number to recover your account.

+

+ Enter your phone number to recover your account. + @if (config('app.recovery_code_expiration_minutes') > 0) +
The code will be available {{ config('app.recovery_code_expiration_minutes') }} minutes. + @endif +

diff --git a/flexiapi/resources/views/errors/410.blade.php b/flexiapi/resources/views/errors/410.blade.php new file mode 100644 index 0000000..8cc7181 --- /dev/null +++ b/flexiapi/resources/views/errors/410.blade.php @@ -0,0 +1,5 @@ +@extends('errors::minimal') + +@section('title', __('Expired resource')) +@section('code', '410') +@section('message', __('The resource you requested is expired')) diff --git a/flexiapi/resources/views/errors/minimal.blade.php b/flexiapi/resources/views/errors/minimal.blade.php index c6f9cc1..be74c4b 100644 --- a/flexiapi/resources/views/errors/minimal.blade.php +++ b/flexiapi/resources/views/errors/minimal.blade.php @@ -6,7 +6,7 @@

@yield('message') -
+

Go back to the homepage diff --git a/flexiapi/resources/views/mails/authentication.blade.php b/flexiapi/resources/views/mails/authentication.blade.php index d55ed25..cc7e60b 100644 --- a/flexiapi/resources/views/mails/authentication.blade.php +++ b/flexiapi/resources/views/mails/authentication.blade.php @@ -11,6 +11,11 @@

{{ $recovery_code }}

+ @if ($expiration_minutes > 0) +

+ The code is only available {{ $expiration_minutes }} minutes. +

+ @endif

You can as well configure your new device using the following code or by directly flashing the QRCode:
diff --git a/flexiapi/resources/views/mails/authentication_text.blade.php b/flexiapi/resources/views/mails/authentication_text.blade.php index 6589fca..196a95b 100644 --- a/flexiapi/resources/views/mails/authentication_text.blade.php +++ b/flexiapi/resources/views/mails/authentication_text.blade.php @@ -5,6 +5,10 @@ Please enter the code bellow to finish the authentication process. {{ $recovery_code }} +@if ($expiration_minutes > 0) +The code is only available {{ $expiration_minutes }} minutes. +@endif + You can as well configure your new device using the following code or by directly flashing the QRCode in the following link: {{ $provisioning_qrcode}} diff --git a/flexiapi/tests/Feature/AccountProvisioningTest.php b/flexiapi/tests/Feature/AccountProvisioningTest.php index 42fa7bd..e143bc3 100644 --- a/flexiapi/tests/Feature/AccountProvisioningTest.php +++ b/flexiapi/tests/Feature/AccountProvisioningTest.php @@ -22,6 +22,8 @@ namespace Tests\Feature; use App\Account; use App\AuthToken; use App\Password; +use App\ProvisioningToken; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class AccountProvisioningTest extends TestCase @@ -134,9 +136,6 @@ class AccountProvisioningTest extends TestCase // Regenerate a new provisioning token from the authenticated account $this->keyAuthenticated($password->account) - ->withHeaders([ - 'x-linphone-provisioning' => true, - ]) ->get('/api/accounts/me/provision') ->assertStatus(200) ->assertSee('provisioning_token') @@ -230,9 +229,6 @@ class AccountProvisioningTest extends TestCase $admin->generateApiKey(); $this->keyAuthenticated($admin) - ->withHeaders([ - 'x-linphone-provisioning' => true, - ]) ->json($this->method, '/api/accounts/' . $password->account->id . '/provision') ->assertStatus(200) ->assertSee('provisioning_token') @@ -255,10 +251,7 @@ class AccountProvisioningTest extends TestCase public function testAuthTokenProvisioning() { // Generate a public auth_token and attach it - $response = $this->withHeaders([ - 'x-linphone-provisioning' => true, - ]) - ->json('POST', '/api/accounts/auth_token') + $response = $this->json('POST', '/api/accounts/auth_token') ->assertStatus(201) ->assertJson([ 'token' => true @@ -270,9 +263,6 @@ class AccountProvisioningTest extends TestCase $password->account->generateApiKey(); $this->keyAuthenticated($password->account) - ->withHeaders([ - 'x-linphone-provisioning' => true, - ]) ->json($this->method, '/api/accounts/auth_token/' . $authToken . '/attach') ->assertStatus(200); @@ -296,4 +286,38 @@ class AccountProvisioningTest extends TestCase ->get($this->route . '/auth_token/' . $authToken) ->assertStatus(404); } + + public function testTokenExpiration() + { + $account = Account::factory()->create(); + $account->generateApiKey(); + $expirationMinutes = 10; + + $this->keyAuthenticated($account) + ->get('/api/accounts/me/provision') + ->assertStatus(200) + ->assertJson([ + 'provisioning_token_expire_at' => null + ]); + + config()->set('app.provisioning_token_expiration_minutes', $expirationMinutes); + + $this->keyAuthenticated($account) + ->get('/api/accounts/me/provision') + ->assertStatus(200) + ->assertJson([ + 'provisioning_token_expire_at' => $account->currentProvisioningToken->created_at->addMinutes($expirationMinutes)->toJSON() + ]); + + $account->refresh(); + + ProvisioningToken::where('id', $account->currentProvisioningToken->id) + ->update(['created_at' => $account->currentProvisioningToken->created_at->subMinutes(1000)]); + + $this->withHeaders([ + 'x-linphone-provisioning' => true, + ]) + ->get($this->route . '/' . $account->provisioning_token) + ->assertStatus(410); + } } diff --git a/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php b/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php index 0d50360..aea80ff 100644 --- a/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php +++ b/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php @@ -91,11 +91,19 @@ class ApiAccountCreationTokenTest extends TestCase $response = $this->keyAuthenticated($admin) ->json($this->method, $this->adminRoute) - ->assertStatus(201); + ->assertStatus(201) + ->assertJson(['expire_at' => null]); $this->assertDatabaseHas('account_creation_tokens', [ 'token' => $response->json()['token'] ]); + + config()->set('app.account_creation_token_expiration_minutes', 10); + + $response = $this->keyAuthenticated($admin) + ->json($this->method, $this->adminRoute) + ->assertStatus(201) + ->assertJson(['expire_at' => AccountCreationToken::latest()->first()->expire_at]); } public function testInvalidToken() @@ -103,31 +111,28 @@ class ApiAccountCreationTokenTest extends TestCase $token = AccountCreationToken::factory()->create(); // Invalid token - $response = $this->json($this->method, $this->accountRoute, [ + $this->json($this->method, $this->accountRoute, [ 'username' => 'username', 'algorithm' => 'SHA-256', 'password' => '123', 'account_creation_token' => '0123456789abc' - ]); - $response->assertStatus(422); + ])->assertStatus(422); // Valid token - $response = $this->json($this->method, $this->accountRoute, [ + $this->json($this->method, $this->accountRoute, [ 'username' => 'username', 'algorithm' => 'SHA-256', 'password' => '123', 'account_creation_token' => $token->token - ]); - $response->assertStatus(200); + ])->assertStatus(200); // Expired token - $response = $this->json($this->method, $this->accountRoute, [ + $this->json($this->method, $this->accountRoute, [ 'username' => 'username2', 'algorithm' => 'SHA-256', 'password' => '123', 'account_creation_token' => $token->token - ]); - $response->assertStatus(422); + ])->assertStatus(422); $this->assertDatabaseHas('account_creation_tokens', [ 'used' => true, @@ -135,6 +140,21 @@ class ApiAccountCreationTokenTest extends TestCase ]); } + public function testTokenExpiration() + { + $token = AccountCreationToken::factory()->expired()->create(); + + config()->set('app.account_creation_token_expiration_minutes', 10); + + $this->json($this->method, $this->accountRoute, [ + 'username' => 'username', + 'algorithm' => 'SHA-256', + 'password' => '123', + 'account_creation_token' => $token->token + ])->assertStatus(422) + ->assertJsonValidationErrors(['account_creation_token']); + } + public function testBlacklistedUsername() { $token = AccountCreationToken::factory()->create(); @@ -142,33 +162,28 @@ class ApiAccountCreationTokenTest extends TestCase config()->set('app.blacklisted_usernames', 'foobar,blacklisted,username-.*'); // Blacklisted username - $response = $this->json($this->method, $this->accountRoute, [ + $this->json($this->method, $this->accountRoute, [ 'username' => 'blacklisted', 'algorithm' => 'SHA-256', 'password' => '123', 'account_creation_token' => $token->token - ]); - $response->assertJsonValidationErrors(['username']); + ])->assertJsonValidationErrors(['username']); // Blacklisted regex username - $response = $this->json($this->method, $this->accountRoute, [ + $this->json($this->method, $this->accountRoute, [ 'username' => 'username-gnap', 'algorithm' => 'SHA-256', 'password' => '123', 'account_creation_token' => $token->token - ]); - - $response->assertJsonValidationErrors(['username']); + ])->assertJsonValidationErrors(['username']); // Valid username - $response = $this->json($this->method, $this->accountRoute, [ + $this->json($this->method, $this->accountRoute, [ 'username' => 'valid-username', 'algorithm' => 'SHA-256', 'password' => '123', 'account_creation_token' => $token->token - ]); - - $response->assertStatus(200); + ])->assertStatus(200); } public function testAccountCreationRequestToken() diff --git a/flexiapi/tests/Feature/ApiAccountEmailChangeTest.php b/flexiapi/tests/Feature/ApiAccountEmailChangeTest.php index dde1834..d7459ef 100644 --- a/flexiapi/tests/Feature/ApiAccountEmailChangeTest.php +++ b/flexiapi/tests/Feature/ApiAccountEmailChangeTest.php @@ -74,6 +74,29 @@ class ApiAccountEmailChangeTest extends TestCase ])->assertJsonValidationErrors(['email']); } + public function testCodeExpiration() + { + $account = Account::factory()->withConsumedAccountCreationToken()->create(); + $account->generateApiKey(); + + $this->keyAuthenticated($account) + ->json($this->method, $this->route.'/request', [ + 'email' => 'new@email.com' + ]) + ->assertStatus(200); + + config()->set('app.email_change_code_expiration_minutes', 10); + + EmailChangeCode::where('id', $account->emailChangeCode->id) + ->update(['created_at' => $account->emailChangeCode->created_at->subMinutes(1000)]); + + $this->keyAuthenticated($account) + ->json($this->method, $this->route, [ + 'code' => $account->emailChangeCode->code + ]) + ->assertStatus(410); + } + public function testUnvalidatedAccount() { $account = Account::factory()->create(); diff --git a/flexiapi/tests/Feature/ApiAccountPhoneChangeTest.php b/flexiapi/tests/Feature/ApiAccountPhoneChangeTest.php index 8a95bf7..1988f3f 100644 --- a/flexiapi/tests/Feature/ApiAccountPhoneChangeTest.php +++ b/flexiapi/tests/Feature/ApiAccountPhoneChangeTest.php @@ -48,6 +48,29 @@ class ApiAccountPhoneChangeTest extends TestCase ->assertStatus(200);*/ } + public function testCodeExpiration() + { + $account = Account::factory()->withConsumedAccountCreationToken()->create(); + $account->generateApiKey(); + + $this->keyAuthenticated($account) + ->json($this->method, $this->route.'/request', [ + 'phone' => '+123123' + ]) + ->assertStatus(200); + + config()->set('app.phone_change_code_expiration_minutes', 10); + + PhoneChangeCode::where('id', $account->phoneChangeCode->id) + ->update(['created_at' => $account->phoneChangeCode->created_at->subMinutes(1000)]); + + $this->keyAuthenticated($account) + ->json($this->method, $this->route, [ + 'code' => $account->phoneChangeCode->code + ]) + ->assertStatus(410); + } + public function testUnvalidatedAccount() { $account = Account::factory()->create();