Resolve properly the domain/realm when hashing the password

Add aliases support through two new endpoints, allowing user to set a phone number on his account
Hide the confirmation_key from the returned account JSON
Bump version number
This commit is contained in:
Timothée Jaussoin 2021-02-11 16:06:20 +01:00
parent bc3d1d1f38
commit 4fc6aaa824
20 changed files with 321 additions and 35 deletions

View file

@ -39,9 +39,10 @@ class Account extends Authenticatable
use HasFactory;
protected $connection = 'external';
protected $with = ['passwords', 'admin', 'emailChanged'];
protected $with = ['passwords', 'admin', 'emailChanged', 'alias'];
protected $hidden = ['alias', 'expire_time', 'confirmation_key'];
protected $dateTimes = ['creation_time'];
protected $appends = ['realm'];
protected $appends = ['realm', 'phone'];
protected $casts = [
'activated' => 'boolean',
];
@ -72,6 +73,11 @@ class Account extends Authenticatable
return $query->where('id', '<', 0);
}
public function phoneChangeCode()
{
return $this->hasOne('App\PhoneChangeCode');
}
public function passwords()
{
return $this->hasMany('App\Password');
@ -112,6 +118,20 @@ class Account extends Authenticatable
return config('app.realm');
}
public function getResolvedRealmAttribute()
{
return config('app.realm') ?? $this->domain;
}
public function getPhoneAttribute()
{
if ($this->alias) {
return $this->alias->alias;
}
return null;
}
public function requestEmailUpdate(string $newEmail)
{
// Remove all the old requests
@ -150,7 +170,7 @@ class Account extends Authenticatable
$password = new Password;
$password->account_id = $this->id;
$password->password = Utils::bchash($this->username, $this->domain, $newPassword, $algorithm);
$password->password = Utils::bchash($this->username, $this->resolvedRealm, $newPassword, $algorithm);
$password->algorithm = $algorithm;
$password->save();
}

View file

@ -23,8 +23,6 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Account;
use App\Password;
use App\Helpers\Utils;
use App\Mail\ConfirmedRegistration;
@ -56,7 +54,7 @@ class PasswordController extends Controller
// If one of the password stored equals the one entered
if (hash_equals(
$password->password,
Utils::bchash($account->username, $account->domain, $request->get('old_password'), $password->algorithm)
Utils::bchash($account->username, $account->resolvedRealm, $request->get('old_password'), $password->algorithm)
)) {
$account->updatePassword($request->get('password'), $algorithm);
$request->session()->flash('success', 'Password successfully changed');

View file

@ -28,7 +28,6 @@ use Carbon\Carbon;
use App\Account;
use App\Alias;
use App\Rules\SIP;
use App\Rules\WithoutSpaces;
use App\Helpers\Utils;
use App\Libraries\OvhSMS;

View file

@ -0,0 +1,84 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 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\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Rules\WithoutSpaces;
use App\Helpers\Utils;
use App\Libraries\OvhSMS;
use App\PhoneChangeCode;
use App\Alias;
class AccountPhoneController extends Controller
{
public function requestUpdate(Request $request)
{
$request->validate([
'phone' => [
'required', 'unique:external.aliases,alias',
'unique:external.accounts,username',
new WithoutSpaces, 'starts_with:+', 'phone:AUTO'
]
]);
$account = $request->user();
$phoneChangeCode = $account->phoneChangeCode ?? new PhoneChangeCode;
$phoneChangeCode->account_id = $account->id;
$phoneChangeCode->phone = $request->get('phone');
$phoneChangeCode->code = Utils::generatePin();
$phoneChangeCode->save();
$ovhSMS = new OvhSMS;
$ovhSMS->send($request->get('phone'), 'Your ' . config('app.name') . ' validation code is ' . $phoneChangeCode->code);
}
public function update(Request $request)
{
$request->validate([
'code' => 'required|digits:4'
]);
$account = $request->user();
$phoneChangeCode = $account->phoneChangeCode()->firstOrFail();
if ($phoneChangeCode->code == $request->get('code')) {
$account->alias()->delete();
$alias = new Alias;
$alias->alias = $phoneChangeCode->phone;
$alias->domain = config('app.sip_domain');
$alias->account_id = $account->id;
$alias->save();
$phoneChangeCode->delete();
$account->refresh();
return $account;
}
$phoneChangeCode->delete();
abort(403);
}
}

View file

@ -107,7 +107,7 @@ class AccountController extends Controller
$password = new Password;
$password->account_id = $account->id;
$password->password = Utils::bchash($account->username, $account->domain, $request->get('password'), $request->get('algorithm'));
$password->password = Utils::bchash($account->username, $account->resolvedRealm, $request->get('password'), $request->get('algorithm'));
$password->algorithm = $request->get('algorithm');
$password->save();

View file

@ -30,7 +30,7 @@ class PasswordController extends Controller
foreach ($account->passwords as $password) {
if (hash_equals(
$password->password,
Utils::bchash($account->username, $account->domain, $request->get('old_password'), $password->algorithm)
Utils::bchash($account->username, $account->resolvedRealm, $request->get('old_password'), $password->algorithm)
)) {
$account->updatePassword($request->get('password'), $algorithm);
return response()->json();

View file

@ -22,7 +22,6 @@ namespace App\Http\Middleware;
use App\Account;
use App\Helpers\Utils;
use Fabiang\Sasl\Sasl;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Response;
@ -45,7 +44,7 @@ class AuthenticateDigestOrKey
*/
public function handle($request, Closure $next)
{
$validator = Validator::make(['from' => $request->header('From')], [
Validator::make(['from' => $request->header('From')], [
'from' => 'required',
])->validate();
@ -95,7 +94,7 @@ class AuthenticateDigestOrKey
$storedNonce->save();
// Validation
$validator = Validator::make($auth, [
Validator::make($auth, [
'opaque' => 'required|in:'.$this->getOpaque(),
//'uri' => 'in:/'.$request->path(),
'qop' => 'required|in:auth',

View file

@ -19,7 +19,6 @@
namespace App\Libraries;
use App\Device;
use Ovh\Api;
class OvhSMS
@ -55,9 +54,9 @@ class OvhSMS
'validityPeriod' => 2880
];
$resultPostJob = $this->_api->post('/sms/'. $this->_smsService . '/jobs', $content);
$this->_api->post('/sms/'. $this->_smsService . '/jobs', $content);
// One credit removed
$smsJobs = $this->_api->get('/sms/'. $this->_smsService . '/jobs');
$this->_api->get('/sms/'. $this->_smsService . '/jobs');
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PhoneChangeCode extends Model
{
use HasFactory;
protected $connection = 'local';
public function account()
{
return $this->belongsTo('App\Account');
}
}

BIN
flexiapi/composer.phar Executable file

Binary file not shown.

View file

@ -21,7 +21,6 @@ namespace Database\Factories;
use App\Account;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class AccountFactory extends Factory
{

View file

@ -21,10 +21,7 @@ namespace Database\Factories;
use App\Account;
use App\Password;
use App\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
class PasswordFactory extends Factory
{

View file

@ -0,0 +1,42 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 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 Database\Factories;
use App\Helpers\Utils;
use App\Password;
use App\PhoneChangeCode;
use Illuminate\Database\Eloquent\Factories\Factory;
class PhoneChangeCodeFactory extends Factory
{
protected $model = PhoneChangeCode::class;
public function definition()
{
$password = Password::factory()->create();
$password->account->generateApiKey();
return [
'account_id' => $password->account->id,
'code' => Utils::generatePin(),
'phone' => '+3312341234',
];
}
}

View file

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddPhoneChangeCodesTable extends Migration
{
public function up()
{
Schema::connection('local')->create('phone_change_codes', function (Blueprint $table) {
$table->bigIncrements('id');
$table->integer('account_id')->unsigned();
$table->string('code');
$table->string('phone');
$table->timestamps();
});
}
public function down()
{
Schema::connection('local')->dropIfExists('phone_change_codes');
}
}

View file

@ -100,6 +100,24 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
<li><code>password</code> required, the new password</li>
</ul>
<h4>Phone number</h4>
<h4><code>POST /accounts/me/phone/request</code></h4>
<p>Request a specific code by SMS</p>
<p>JSON parameters:</p>
<ul>
<li><code>phone</code> the phone number to send the SMS</li>
</ul>
<h4><code>POST /accounts/me/phone</code></h4>
<p>Confirm the code received and change the phone number</p>
<p>JSON parameters:</p>
<ul>
<li><code>code</code> the received SMS code</li>
</ul>
<p>Return the updated account</p>
<h4>Devices</h4>
<h4><code>GET /accounts/me/devices</code></h4>

View file

@ -34,6 +34,9 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () {
Route::get('accounts/me', 'Api\AccountController@show');
Route::delete('accounts/me', 'Api\AccountController@delete');
Route::post('accounts/me/phone/request', 'Api\AccountPhoneController@requestUpdate');
Route::post('accounts/me/phone', 'Api\AccountPhoneController@update');
Route::get('accounts/me/devices', 'Api\DeviceController@index');
Route::delete('accounts/me/devices/{uuid}', 'Api\DeviceController@destroy');

View file

@ -22,12 +22,9 @@ namespace Tests\Feature;
use App\Password;
use App\Account;
use App\Admin;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use Illuminate\Support\Facades\DB;
class AccountApiTest extends TestCase
{
@ -38,7 +35,7 @@ class AccountApiTest extends TestCase
public function testMandatoryFrom()
{
$password = Password::factory()->create();
Password::factory()->create();
$response = $this->json($this->method, $this->route);
$response->assertStatus(422);
}
@ -109,8 +106,6 @@ class AccountApiTest extends TestCase
'domain' => $domain,
'activated' => false
]);
$this->assertFalse(empty($response1['confirmation_key']));
}
public function testUsernameNoDomain()
@ -179,8 +174,6 @@ class AccountApiTest extends TestCase
'domain' => config('app.sip_domain'),
'activated' => true,
]);
$this->assertTrue(empty($response1['confirmation_key']));
}
public function testNotActivated()
@ -208,8 +201,6 @@ class AccountApiTest extends TestCase
'domain' => config('app.sip_domain'),
'activated' => false,
]);
$this->assertFalse(empty($response1['confirmation_key']));
}
public function testSimpleAccount()
@ -424,7 +415,7 @@ class AccountApiTest extends TestCase
]);
// Set the new password with incorrect old password
$response = $this->keyAuthenticated($account)
$this->keyAuthenticated($account)
->json($this->method, $this->route.'/me/password', [
'algorithm' => $newAlgorithm,
'old_password' => 'blabla',
@ -513,7 +504,8 @@ class AccountApiTest extends TestCase
->get($this->route.'/'.$admin->id)
->assertStatus(200)
->assertJson([
'id' => 1
'id' => 1,
'phone' => null
]);
}

View file

@ -0,0 +1,95 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 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 Tests\Feature;
use App\Password;
use App\Account;
use App\Admin;
use App\PhoneChangeCode;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AccountPhoneChangeTest extends TestCase
{
use RefreshDatabase;
protected $route = '/api/accounts/me/phone';
protected $method = 'POST';
public function testRequest()
{
$password = Password::factory()->create();
$password->account->generateApiKey();
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/request', [
'phone' => 'blabla'
])
->assertStatus(422);
// Send a SMS
/*$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/request', [
'phone' => '+33667545663'
])
->assertStatus(200);*/
}
public function testConfirmLongCode()
{
$phoneChange = PhoneChangeCode::factory()->create();
$this->keyAuthenticated($phoneChange->account)
->json($this->method, $this->route, [
'code' => 'wrong'
])
->assertStatus(422);
}
public function testConfirmGoodCode()
{
$phoneChange = PhoneChangeCode::factory()->create();
$phone = $phoneChange->phone;
$this->keyAuthenticated($phoneChange->account)
->get('/api/accounts/me')
->assertStatus(200)
->assertJson([
'phone' => null
]);
$this->keyAuthenticated($phoneChange->account)
->json($this->method, $this->route, [
'code' => $phoneChange->code
])
->assertStatus(200)
->assertJson([
'phone' => $phone,
]);
$this->keyAuthenticated($phoneChange->account)
->get('/api/accounts/me')
->assertStatus(200)
->assertJson([
'phone' => $phone
]);
}
}

View file

@ -22,7 +22,6 @@ namespace Tests\Feature;
use App\Password;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class AuthenticateDigestAndKeyTest extends TestCase
@ -34,14 +33,14 @@ class AuthenticateDigestAndKeyTest extends TestCase
public function testMandatoryFrom()
{
$password = Password::factory()->create();
Password::factory()->create();
$response = $this->json($this->method, $this->route);
$response->assertStatus(422);
}
public function testWrongFrom()
{
$password = Password::factory()->create();
Password::factory()->create();
$response = $this->withHeaders([
'From' => 'sip:missing@username',
])->json($this->method, $this->route);

View file

@ -8,7 +8,7 @@
#%define _datadir %{_datarootdir}
#%define _docdir %{_datadir}/doc
%define build_number 48
%define build_number 49
%define var_dir /var/opt/belledonne-communications
%define opt_dir /opt/belledonne-communications/share/flexisip-account-manager
%define env_file "$RPM_BUILD_ROOT/etc/flexisip-account-manager/flexiapi.env"