mirror of
https://gitlab.linphone.org/BC/public/flexisip-account-manager.git
synced 2026-01-17 10:08:05 +00:00
Fix FLEXIAPI-216 Implement the RFC 8898 partially... for HTTP
This commit is contained in:
parent
648936514f
commit
61bc04da02
13 changed files with 126 additions and 63 deletions
|
|
@ -9,6 +9,7 @@ v1.6
|
|||
- Fix FLEXIAPI-211 Add a JSON validation middleware + test
|
||||
- Fix FLEXIAPI-212 Add CoTURN credentials support in the provisioning
|
||||
- Fix FLEXIAPI-213 Add TURN credentials support in the API as defined in draft-uberti-behave-turn-rest-00
|
||||
- Fix FLEXIAPI-216 Implement the RFC 8898 partially... for HTTP
|
||||
|
||||
v1.5
|
||||
---
|
||||
|
|
|
|||
|
|
@ -9,41 +9,8 @@ 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_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
|
||||
|
||||
# Risky toggles
|
||||
APP_DANGEROUS_ENDPOINTS=false # Enable some dangerous endpoints used for XMLRPC like fallback usage
|
||||
|
||||
# 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
|
||||
|
||||
# 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_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
|
||||
|
||||
# Account provisioning
|
||||
ACCOUNT_PROVISIONING_RC_FILE=
|
||||
ACCOUNT_PROVISIONING_OVERWRITE_ALL=
|
||||
ACCOUNT_PROVISIONING_USE_X_LINPHONE_PROVISIONING_HEADER=true
|
||||
|
||||
# Blocking service
|
||||
BLOCKING_TIME_PERIOD_CHECK=30 # Time span on which the blocking service will proceed, in minutes
|
||||
BLOCKING_AMOUNT_EVENTS_AUTHORIZED_DURING_PERIOD=5 # Amount of account events authorized during this period
|
||||
|
||||
# Instance specific parameters
|
||||
INSTANCE_COPYRIGHT= # Simple text displayed in the page footer
|
||||
INSTANCE_INTRO_REGISTRATION= # Markdown text displayed in the home page
|
||||
|
|
@ -63,6 +30,39 @@ APP_PROJECT_URL= # A URL pointing to the project information page
|
|||
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
# Risky toggles
|
||||
APP_DANGEROUS_ENDPOINTS=false # Enable some dangerous endpoints used for XMLRPC like fallback usage
|
||||
|
||||
# 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
|
||||
|
||||
# Expiration time for tokens and code, in minutes, 0 means no expiration
|
||||
APP_API_ACCOUNT_CREATION_TOKEN_RETRY_MINUTES=60 # Number of minutes between two consecutive account_creation_token creation
|
||||
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 and authentication
|
||||
ACCOUNT_EMAIL_UNIQUE=false # Emails are unique between all the accounts
|
||||
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
|
||||
ACCOUNT_AUTHENTICATION_BEARER_URL= # URL of the external service that can provide a trusted (eg. JWT token) for the authentication, takes priority and disable the DIGEST auth if set, see https://www.rfc-editor.org/rfc/rfc8898
|
||||
|
||||
# Account provisioning
|
||||
ACCOUNT_PROVISIONING_RC_FILE=
|
||||
ACCOUNT_PROVISIONING_OVERWRITE_ALL=
|
||||
ACCOUNT_PROVISIONING_USE_X_LINPHONE_PROVISIONING_HEADER=true
|
||||
|
||||
# Blocking service
|
||||
BLOCKING_TIME_PERIOD_CHECK=30 # Time span on which the blocking service will proceed, in minutes
|
||||
BLOCKING_AMOUNT_EVENTS_AUTHORIZED_DURING_PERIOD=5 # Amount of account events authorized during this period
|
||||
|
||||
# FlexiSIP database
|
||||
# Ensure that you have the proper SELinux configuration to allow database connections, see the README
|
||||
DB_CONNECTION=mysql
|
||||
|
|
@ -126,3 +126,4 @@ HCAPTCHA_SITEKEY=site-key
|
|||
# JWT
|
||||
JWT_RSA_PUBLIC_KEY_PEM=
|
||||
JWT_SIP_IDENTIFIER=
|
||||
|
||||
|
|
|
|||
|
|
@ -302,12 +302,12 @@ class Account extends Authenticatable
|
|||
|
||||
public function getRealmAttribute()
|
||||
{
|
||||
return config('app.realm');
|
||||
return config('app.account_realm');
|
||||
}
|
||||
|
||||
public function getResolvedRealmAttribute()
|
||||
{
|
||||
return config('app.realm') ?? $this->domain;
|
||||
return config('app.account_realm') ?? $this->domain;
|
||||
}
|
||||
|
||||
public function getConfirmationKeyExpiresAttribute()
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ use Carbon\Carbon;
|
|||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Http\Response;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Closure;
|
||||
use Validator;
|
||||
|
||||
class AuthenticateDigestOrKey
|
||||
|
|
@ -58,9 +58,9 @@ class AuthenticateDigestOrKey
|
|||
return $this->generateUnauthorizedResponse(null, 'Invalid API Key');
|
||||
}
|
||||
|
||||
Validator::make(['from' => $request->header('From')], [
|
||||
'from' => 'required',
|
||||
])->validate();
|
||||
if (empty($request->header('From'))) {
|
||||
return $this->generateUnauthorizedResponse(null, 'From header is required or invalid token');
|
||||
}
|
||||
|
||||
$from = $this->extractFromHeader($request->header('From'));
|
||||
list($username, $domain) = parseSIP($from);
|
||||
|
|
@ -70,7 +70,7 @@ class AuthenticateDigestOrKey
|
|||
->where('domain', $domain)
|
||||
->firstOrFail();
|
||||
|
||||
$resolvedRealm = config('app.realm') ?? $domain;
|
||||
$resolvedRealm = config('app.account_realm') ?? $domain;
|
||||
|
||||
// DIGEST authentication
|
||||
|
||||
|
|
@ -132,7 +132,8 @@ class AuthenticateDigestOrKey
|
|||
: $password->password; // username:realm/domain:password
|
||||
$a2 = hash($hash, $request->method().':'.$auth['uri']);
|
||||
|
||||
$validResponse = hash($hash,
|
||||
$validResponse = hash(
|
||||
$hash,
|
||||
$a1.
|
||||
':'.$auth['nonce'].
|
||||
':'.$auth['nc'].
|
||||
|
|
@ -161,7 +162,7 @@ class AuthenticateDigestOrKey
|
|||
|
||||
private function generateUnauthorizedResponse(?Account $account = null, $message = 'Unauthenticated request')
|
||||
{
|
||||
$response = new Response;
|
||||
$response = new Response();
|
||||
|
||||
if ($account) {
|
||||
$nonce = generateValidNonce($account);
|
||||
|
|
@ -198,7 +199,7 @@ class AuthenticateDigestOrKey
|
|||
private function generateAuthHeaders(Account $account, string $nonce): array
|
||||
{
|
||||
$headers = [];
|
||||
$resolvedRealm = config('app.realm') ?? $account->domain;
|
||||
$resolvedRealm = config('app.account_realm') ?? $account->domain;
|
||||
|
||||
foreach ($account->passwords as $password) {
|
||||
if ($password->algorithm == 'CLRTXT') {
|
||||
|
|
@ -209,7 +210,7 @@ class AuthenticateDigestOrKey
|
|||
);
|
||||
}
|
||||
break;
|
||||
} else if (\in_array($password->algorithm, array_keys(passwordAlgorithms()))) {
|
||||
} elseif (\in_array($password->algorithm, array_keys(passwordAlgorithms()))) {
|
||||
array_push(
|
||||
$headers,
|
||||
$this->generateAuthHeader($resolvedRealm, $password->algorithm, $nonce)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use Closure;
|
|||
use DateTimeImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
use Lcobucci\JWT\Encoding\JoseEncoder;
|
||||
use Lcobucci\JWT\Token\Parser;
|
||||
|
|
@ -94,6 +95,21 @@ class AuthenticateJWT
|
|||
}
|
||||
|
||||
Auth::login($account);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (!empty(config('app.account_authentication_bearer_url'))) {
|
||||
$response = new Response();
|
||||
|
||||
$response->header(
|
||||
'WWW-Authenticate',
|
||||
'Bearer authz_server="' . config('app.account_authentication_bearer_url') . '"'
|
||||
);
|
||||
|
||||
$response->setStatusCode(401);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ class BlacklistedUsername implements Rule
|
|||
{
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
if (!empty(config('app.blacklisted_usernames'))) {
|
||||
foreach (explode(',', config('app.blacklisted_usernames')) as $username) {
|
||||
if (!empty(config('app.account_blacklisted_usernames'))) {
|
||||
foreach (explode(',', config('app.account_blacklisted_usernames')) as $username) {
|
||||
if ($value == $username) return false;
|
||||
|
||||
// Regex rules
|
||||
|
|
|
|||
|
|
@ -30,11 +30,18 @@ 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'),
|
||||
|
||||
'account_email_unique' => env('ACCOUNT_EMAIL_UNIQUE', false),
|
||||
'allow_phone_number_username_admin_api' => env('APP_ALLOW_PHONE_NUMBER_USERNAME_ADMIN_API', false),
|
||||
'blacklisted_usernames' => env('ACCOUNT_BLACKLISTED_USERNAMES', ''),
|
||||
'account_blacklisted_usernames' => env('ACCOUNT_BLACKLISTED_USERNAMES', ''),
|
||||
'account_email_unique' => env('ACCOUNT_EMAIL_UNIQUE', false),
|
||||
'account_username_regex' => env('ACCOUNT_USERNAME_REGEX', '^[a-z0-9+_.-]*$'),
|
||||
'account_default_password_algorithm' => env('ACCOUNT_DEFAULT_PASSWORD_ALGORITHM', 'SHA-256'),
|
||||
'account_authentication_bearer_url' => env('ACCOUNT_AUTHENTICATION_BEARER_URL', null),
|
||||
|
||||
/**
|
||||
* Set a global realm for all the accounts, if not set, the account domain
|
||||
* will be used as a fallback
|
||||
*/
|
||||
'account_realm' => env('ACCOUNT_REALM', null),
|
||||
|
||||
/**
|
||||
* Time limit before the API Key and related cookie are expired
|
||||
|
|
@ -77,12 +84,6 @@ return [
|
|||
'provisioning_overwrite_all' => env('ACCOUNT_PROVISIONING_OVERWRITE_ALL', false),
|
||||
'provisioning_use_x_linphone_provisioning_header' => env('ACCOUNT_PROVISIONING_USE_X_LINPHONE_PROVISIONING_HEADER', true),
|
||||
|
||||
/**
|
||||
* 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),
|
||||
|
||||
/**
|
||||
* /!\ Enable dangerous endpoints required for fallback
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class PasswordFactory extends Factory
|
|||
public function definition()
|
||||
{
|
||||
$account = Account::factory()->create();
|
||||
$realm = config('app.realm') ?? $account->domain;
|
||||
$realm = config('app.account_realm') ?? $account->domain;
|
||||
|
||||
return [
|
||||
'account_id' => $account->id,
|
||||
|
|
@ -54,7 +54,7 @@ class PasswordFactory extends Factory
|
|||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$account = Account::find($attributes['account_id']);
|
||||
$realm = config('app.realm') ?? $account->domain;
|
||||
$realm = config('app.account_realm') ?? $account->domain;
|
||||
|
||||
return [
|
||||
'password' => hash('sha256', $account->username.':'.$realm.':testtest'),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ class AccountJWTAuthenticationTest extends TestCase
|
|||
protected $serverPrivateKeyPem = null;
|
||||
protected $serverPublicKeyPem = null;
|
||||
|
||||
protected $routeAccountMe = '/api/accounts/me';
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
|
@ -174,6 +176,46 @@ class AccountJWTAuthenticationTest extends TestCase
|
|||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function testAuthBearerUrl()
|
||||
{
|
||||
$server = 'https://auth_bearer.com/';
|
||||
config()->set('app.account_authentication_bearer_url', $server);
|
||||
|
||||
$password = Password::factory()->create();
|
||||
|
||||
$response = $this->json($this->method, $this->routeAccountMe)
|
||||
->assertStatus(401);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Bearer authz_server="' . $server . '"',
|
||||
$response->headers->all()['www-authenticate'][0]
|
||||
);
|
||||
|
||||
// Wrong From
|
||||
$reponse = $this
|
||||
->withHeaders(['From' => 'sip:missing@username'])
|
||||
->json($this->method, $this->routeAccountMe)
|
||||
->assertStatus(401);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Bearer authz_server="' . $server . '"',
|
||||
$response->headers->all()['www-authenticate'][0]
|
||||
);
|
||||
|
||||
// Wrong bearer message
|
||||
$reponse = $this
|
||||
->withHeaders([
|
||||
'Authorization' => 'Bearer 1234'
|
||||
])
|
||||
->json($this->method, $this->routeAccountMe)
|
||||
->assertStatus(401);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Bearer authz_server="' . $server . '"',
|
||||
$response->headers->all()['www-authenticate'][0]
|
||||
);
|
||||
}
|
||||
|
||||
private function checkToken(UnencryptedToken $token): void
|
||||
{
|
||||
$this->withHeaders([
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class AccountProvisioningTest extends TestCase
|
|||
{
|
||||
$this->withHeaders([
|
||||
'x-linphone-provisioning' => true,
|
||||
])->get($this->accountRoute)->assertStatus(302);
|
||||
])->get($this->accountRoute)->assertStatus(401);
|
||||
}
|
||||
|
||||
public function testAuthenticatedWithPasswordProvisioning()
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ class ApiAccountCreationTokenTest extends TestCase
|
|||
{
|
||||
$token = AccountCreationToken::factory()->create();
|
||||
|
||||
config()->set('app.blacklisted_usernames', 'foobar,blacklisted,username-.*');
|
||||
config()->set('app.account_blacklisted_usernames', 'foobar,blacklisted,username-.*');
|
||||
|
||||
// Blacklisted username
|
||||
$this->json($this->method, $this->accountRoute, [
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ApiAccountTest extends TestCase
|
|||
{
|
||||
Password::factory()->create();
|
||||
$response = $this->json($this->method, $this->route);
|
||||
$response->assertStatus(422);
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
|
||||
public function testNotAdminForbidden()
|
||||
|
|
@ -588,7 +588,7 @@ class ApiAccountTest extends TestCase
|
|||
$password->account->save();
|
||||
|
||||
$realm = 'realm.com';
|
||||
config()->set('app.realm', $realm);
|
||||
config()->set('app.account_realm', $realm);
|
||||
|
||||
/**
|
||||
* Public information
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class ApiAuthenticationTest extends TestCase
|
|||
{
|
||||
Password::factory()->create();
|
||||
$response = $this->json($this->method, $this->route);
|
||||
$response->assertStatus(422);
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
|
||||
public function testWrongFrom()
|
||||
|
|
@ -204,7 +204,7 @@ class ApiAuthenticationTest extends TestCase
|
|||
public function testAuthenticationSHA265FromCLRTXTWithRealm()
|
||||
{
|
||||
$realm = 'realm.com';
|
||||
config()->set('app.realm', $realm);
|
||||
config()->set('app.account_realm', $realm);
|
||||
|
||||
$password = Password::factory()->clrtxt()->create();
|
||||
$response = $this->generateFirstResponse($password);
|
||||
|
|
@ -230,7 +230,8 @@ class ApiAuthenticationTest extends TestCase
|
|||
public function testAuthenticationBadPassword()
|
||||
{
|
||||
$password = Password::factory()->create();
|
||||
$response = $this->generateFirstResponse($password);;
|
||||
$response = $this->generateFirstResponse($password);
|
||||
;
|
||||
$password->password = 'wrong';
|
||||
|
||||
$response = $this->generateSecondResponse($password, $response)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue