Add support of realms in the authentication process through a global configuration variable

This commit is contained in:
Timothée Jaussoin 2021-02-03 15:33:07 +01:00
parent cda7864c52
commit b6959cc5dd
11 changed files with 346 additions and 288 deletions

View file

@ -10,6 +10,7 @@ APP_EVERYONE_IS_ADMIN=false
# SIP server parameters
ACCOUNT_PROXY_REGISTRAR_ADDRESS=sip.example.com # Proxy registrar address, can be different than the SIP domain
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
# Instance specific parameters
INSTANCE_COPYRIGHT= # Simple text displayed in the page footer

View file

@ -94,14 +94,6 @@ If your external database is locate on a remote machine, you should also allow y
setsebool httpd_can_network_connect 1 // Allow remote network connected
setsebool httpd_can_network_connect_db 1 // Allow remote database connection
### CRON job
The DIGEST authentication method is saving some temporary information (nonces) in the database.
To expire and/or clear old nonces a specific command should be called periodically.
php artisan digest:expired-nonces-clear <minutes>
## Usage
The `/api` page contains all the required documentation to authenticate and request the API.

View file

@ -41,6 +41,7 @@ class Account extends Authenticatable
protected $connection = 'external';
protected $with = ['passwords', 'admin', 'emailChanged'];
protected $dateTimes = ['creation_time'];
protected $appends = ['realm'];
protected $casts = [
'activated' => 'boolean',
];
@ -106,6 +107,11 @@ class Account extends Authenticatable
return $this->attributes['username'].'@'.$this->attributes['domain'];
}
public function getRealmAttribute()
{
return config('app.realm');
}
public function requestEmailUpdate(string $newEmail)
{
// Remove all the old requests

View file

@ -35,7 +35,8 @@ class AccountController extends Controller
$account = Account::sip($sip)->firstOrFail();
return \response()->json([
'activated' => $account->activated
'activated' => $account->activated,
'realm' => $account->realm
]);
}

View file

@ -56,6 +56,8 @@ class AuthenticateDigestOrKey
->where('domain', $domain)
->firstOrFail();
$resolvedRealm = config('app.realm') ?? $domain;
// Check if activated
if (!$account->activated) {
return $this->generateUnauthorizedResponse($account);
@ -97,7 +99,7 @@ class AuthenticateDigestOrKey
'opaque' => 'required|in:'.$this->getOpaque(),
//'uri' => 'in:/'.$request->path(),
'qop' => 'required|in:auth',
'realm' => 'required|in:'.$domain,
'realm' => 'required|in:'.$resolvedRealm,
'nc' => 'required',
'cnonce' => 'required',
'algorithm' => [
@ -126,8 +128,8 @@ class AuthenticateDigestOrKey
// Hashing and checking
$A1 = $password->algorithm == 'CLRTXT'
? hash($hash, $account->username.':'.$account->domain.':'.$password->password)
: $password->password; // username:domain:password
? hash($hash, $account->username.':'.$resolvedRealm.':'.$password->password)
: $password->password; // username:realm/domain:password
$A2 = hash($hash, $request->method().':'.$auth['uri']);
$validResponse = hash($hash,
@ -194,20 +196,21 @@ class AuthenticateDigestOrKey
private function generateAuthHeaders(Account $account, string $nonce): array
{
$headers = [];
$resolvedRealm = config('app.realm') ?? $account->domain;
foreach ($account->passwords as $password) {
if ($password->algorithm == 'CLRTXT') {
foreach (array_keys(self::ALGORITHMS) as $algorithm) {
array_push(
$headers,
$this->generateAuthHeader($account->domain, $algorithm, $nonce)
$this->generateAuthHeader($resolvedRealm, $algorithm, $nonce)
);
}
break;
} else if (\in_array($password->algorithm, array_keys(self::ALGORITHMS))) {
array_push(
$headers,
$this->generateAuthHeader($account->domain, $password->algorithm, $nonce)
$this->generateAuthHeader($resolvedRealm, $password->algorithm, $nonce)
);
}
}

551
flexiapi/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,12 @@ return [
'proxy_registrar_address' => env('ACCOUNT_PROXY_REGISTRAR_ADDRESS', 'sip.domain.com'),
'transport_protocol_text' => env('ACCOUNT_TRANSPORT_PROTOCOL_TEXT', 'TLS (recommended), TCP or UDP'),
/**
* Set a global realm for all the accounts, if not set, the account domain
* will be used as a fallback
*/
'realm' => env('ACCOUNT_REALM', null),
/**
* Allow any accounts to request the API as an administrator
* This parameter is only the for debug purpose or running the tests

View file

@ -33,10 +33,11 @@ class PasswordFactory extends Factory
public function definition()
{
$account = Account::factory()->create();
$realm = config('app.realm') ?? $account->domain;
return [
'account_id' => $account->id,
'password' => hash('md5', $account->username.':'.$account->domain.':testtest'),
'password' => hash('md5', $account->username.':'.$realm.':testtest'),
'algorithm' => 'MD5',
];
}
@ -45,9 +46,10 @@ class PasswordFactory extends Factory
{
return $this->state(function (array $attributes) {
$account = Account::find($attributes['account_id']);
$realm = config('app.realm') ?? $account->domain;
return [
'password' => hash('sha256', $account->username.':'.$account->domain.':testtest'),
'password' => hash('sha256', $account->username.':'.$realm.':testtest'),
'account_id' => $account->id,
'algorithm' => 'SHA-256',
];

View file

@ -219,13 +219,17 @@ class AccountApiTest extends TestCase
$password->account->generateApiKey();
$password->account->save();
$realm = 'realm.com';
config()->set('app.realm', $realm);
/**
* Public information
*/
$this->get($this->route.'/'.$password->account->identifier.'/info')
->assertStatus(200)
->assertJson([
'activated' => false
'activated' => false,
'realm' => $realm
]);
$password->account->activated = true;
@ -239,7 +243,8 @@ class AccountApiTest extends TestCase
->assertStatus(200)
->assertJson([
'username' => $password->account->username,
'activated' => true
'activated' => true,
'realm' => $realm
]);
/**

View file

@ -142,6 +142,7 @@ class AuthenticateDigestAndKeyTest extends TestCase
public function testAuthenticationMD5()
{
$password = Password::factory()->create();
$response = $this->generateFirstResponse($password);
$response = $this->generateSecondResponse($password, $response)
->json($this->method, $this->route);
@ -168,7 +169,7 @@ class AuthenticateDigestAndKeyTest extends TestCase
public function testAuthenticationSHA265FromCLRTXT()
{
$password = Password::factory()->clrtxt()->create();
$response = $this->generateFirstResponse($password);;
$response = $this->generateFirstResponse($password);
// The server is generating all the available hash algorythms
$this->assertStringContainsString('algorithm=MD5', $response->headers->all()['www-authenticate'][0]);
@ -192,6 +193,32 @@ class AuthenticateDigestAndKeyTest extends TestCase
$response->assertStatus(200);
}
public function testAuthenticationSHA265FromCLRTXTWithRealm()
{
$realm = 'realm.com';
config()->set('app.realm', $realm);
$password = Password::factory()->clrtxt()->create();
$response = $this->generateFirstResponse($password);
// Let's simulate a local hash for the clear password
$hash = 'sha256';
$password->password = hash(
$hash,
$password->account->username.':'.$realm.':'.$password->password
);
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response, $hash),
])->json($this->method, $this->route);
$this->assertStringContainsString('algorithm=MD5', $response->headers->all()['www-authenticate'][0]);
$this->assertStringContainsString('algorithm=SHA-256', $response->headers->all()['www-authenticate'][1]);
$response->assertStatus(200);
}
public function testAuthenticationBadPassword()
{
$password = Password::factory()->create();

View file

@ -8,7 +8,7 @@
#%define _datadir %{_datarootdir}
#%define _docdir %{_datadir}/doc
%define build_number 44
%define build_number 45
%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"