Fix FLEXIAPI-192 Add DotEnv configuration to allow the expiration of tokens and codes in the app

This commit is contained in:
Timothée Jaussoin 2024-07-11 12:51:02 +00:00
parent 29fc1744a3
commit f6c5562201
46 changed files with 486 additions and 66 deletions

View file

@ -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

View file

@ -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

View file

@ -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'];

View file

@ -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()
{

View file

@ -17,14 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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(

View file

@ -17,14 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Console\Commands;
namespace App\Console\Commands\Accounts;
use Illuminate\Console\Command;
use Carbon\Carbon;

View file

@ -17,16 +17,15 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';

View file

@ -17,13 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';

View file

@ -17,13 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Console\Commands;
namespace App\Console\Commands\Digest;
use Illuminate\Console\Command;
use Carbon\Carbon;

View file

@ -17,12 +17,12 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';

View file

@ -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;
}
}

View file

@ -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()

View file

@ -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;

View file

@ -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'

View file

@ -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
]
]);

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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
{

View file

@ -1,10 +1,28 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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;
}

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Account\Create;

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Account\Create\Web;

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Account\Create\Web;

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Account\Update\Api;

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Account\Update;

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Account\Update\Web;

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests;

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests;

View file

@ -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,

View file

@ -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()

View file

@ -25,6 +25,7 @@ class ProvisioningToken extends Consommable
{
use HasFactory;
protected ?string $configExpirationMinutesKey = 'provisioning_token_expiration_minutes';
protected $casts = [
'used' => 'boolean',
];

View file

@ -7,4 +7,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
class RecoveryCode extends Consommable
{
use HasFactory;
protected ?string $configExpirationMinutesKey = 'recovery_code_expiration_minutes';
}

View file

@ -0,0 +1,38 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';
}
}

View file

@ -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;

View file

@ -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
*/

5
flexiapi/config/rcfile Normal file
View file

@ -0,0 +1,5 @@
[auth_info_0]
test=foobar
[auth_info_1]
blabla=gnap

View file

@ -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)
]);
}
}

View file

@ -9,7 +9,12 @@
@if ($method == 'email')
<div class="large">
<p class="large">Enter your email account to recover it.</p>
<p class="large">
Enter your email account to recover it.
@if (config('app.recovery_code_expiration_minutes') > 0)
<br /> The code will be available {{ config('app.recovery_code_expiration_minutes') }} minutes.
@endif
</p>
@include('parts.errors', ['name' => 'code'])
</div>
<div class="large">
@ -29,7 +34,12 @@
</div>
@endif
@elseif($method == 'phone')
<p class="large">Enter your phone number to recover your account.</p>
<p class="large">
Enter your phone number to recover your account.
@if (config('app.recovery_code_expiration_minutes') > 0)
<br />The code will be available {{ config('app.recovery_code_expiration_minutes') }} minutes.
@endif
</p>
<div>
<input placeholder="+123456789" name="phone" type="text" value="{{ old('phone') }}">
<label for="phone">Phone</label>

View file

@ -0,0 +1,5 @@
@extends('errors::minimal')
@section('title', __('Expired resource'))
@section('code', '410')
@section('message', __('The resource you requested is expired'))

View file

@ -6,7 +6,7 @@
<p class="text-center">
@yield('message')
<br />
<br /><br />
<a class="btn btn-secondary mt-5" href="{{ route('account.home') }}">
Go back to the homepage
</a>

View file

@ -11,6 +11,11 @@
<p>
<h2>{{ $recovery_code }}</h2>
</p>
@if ($expiration_minutes > 0)
<p>
The code is only available {{ $expiration_minutes }} minutes.
</p>
@endif
<p>
You can as well configure your new device using the following code or by directly flashing the QRCode:<br />

View file

@ -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}}

View file

@ -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);
}
}

View file

@ -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()

View file

@ -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();

View file

@ -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();