diff --git a/flexiapi/app/Http/Controllers/Api/AccountController.php b/flexiapi/app/Http/Controllers/Api/AccountController.php
new file mode 100644
index 0000000..a20c6aa
--- /dev/null
+++ b/flexiapi/app/Http/Controllers/Api/AccountController.php
@@ -0,0 +1,43 @@
+validate([
+ 'username' => 'required|unique:external.accounts,username|min:6',
+ 'algorithm' => 'required|in:SHA-256,MD5',
+ 'password' => 'required|min:6',
+ ]);
+
+ $algorithm = $request->has('password_sha256') ? 'SHA-256' : 'MD5';
+
+ $account = new Account;
+ $account->username = $request->get('username');
+ $account->email = $request->get('email');
+ $account->activated = true;
+ $account->domain = config('app.sip_domain');
+ $account->ip_address = $request->ip();
+ $account->creation_time = Carbon::now();
+ $account->user_agent = config('app.name');
+ $account->save();
+
+ $password = new Password;
+ $password->account_id = $account->id;
+ $password->password = Utils::bchash($account->username, $account->domain, $request->get('password'), $request->get('algorithm'));
+ $password->algorithm = $request->get('algorithm');
+ $password->save();
+
+ return response()->json($account);
+ }
+}
diff --git a/flexiapi/database/factories/AdminFactory.php b/flexiapi/database/factories/AdminFactory.php
new file mode 100644
index 0000000..3d482c7
--- /dev/null
+++ b/flexiapi/database/factories/AdminFactory.php
@@ -0,0 +1,29 @@
+.
+*/
+
+use App\Admin;
+use Faker\Generator as Faker;
+
+$factory->define(Admin::class, function (Faker $faker) use ($factory) {
+ $password = $factory->create(App\Password::class);
+
+ return [
+ 'account_id' => $password->account_id,
+ ];
+});
diff --git a/flexiapi/resources/views/documentation.blade.php b/flexiapi/resources/views/documentation.blade.php
index 64fc3dd..e51a351 100644
--- a/flexiapi/resources/views/documentation.blade.php
+++ b/flexiapi/resources/views/documentation.blade.php
@@ -32,7 +32,27 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
Endpoints
-Current implemented endpoints
+Accounts
+
+POST /accounts
+
+JSON parameters:
+
+
+ username unique username, minimum 6 characters
+ password required minimum 6 characters
+ algorithm required, values can be SHA-256 or MD5
+
+
+To create an account directly from the API.
This endpoint is authenticated and requires an admin account.
+
+Ping
+
+GET /ping
+
+Returns pong
+
+Devices
GET /devices
diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php
index a976b3f..a818790 100644
--- a/flexiapi/routes/api.php
+++ b/flexiapi/routes/api.php
@@ -29,4 +29,8 @@ Route::group(['middleware' => ['auth.digest']], function () {
Route::get('ping', 'Api\PingController@ping');
Route::get('devices', 'Api\DeviceController@index');
Route::delete('devices/{uuid}', 'Api\DeviceController@destroy');
+
+ Route::group(['middleware' => ['auth.admin']], function () {
+ Route::post('accounts', 'Api\AccountController@store');
+ });
});
\ No newline at end of file
diff --git a/flexiapi/tests/Feature/AccountApiTest.php b/flexiapi/tests/Feature/AccountApiTest.php
new file mode 100644
index 0000000..0b07081
--- /dev/null
+++ b/flexiapi/tests/Feature/AccountApiTest.php
@@ -0,0 +1,68 @@
+create();
+ $response = $this->json($this->method, $this->route);
+ $response->assertStatus(422);
+ }
+
+ public function testNotAdminForbidden()
+ {
+ $password = factory(Password::class)->create();
+ $response0 = $this->withHeaders([
+ 'From' => 'sip:'.$password->account->identifier
+ ])->json($this->method, $this->route);
+
+ $response1 = $this->withHeaders([
+ 'From' => 'sip:'.$password->account->identifier,
+ 'Authorization' => $this->generateDigest($password, $response0),
+ ])->json($this->method, $this->route);
+
+ $response1->assertStatus(403);
+ }
+
+ public function testAdminOk()
+ {
+ $admin = factory(Admin::class)->create();
+ $password = $admin->account->passwords()->first();
+
+ $response0 = $this->withHeaders([
+ 'From' => 'sip:'.$password->account->identifier
+ ])->json($this->method, $this->route);
+
+ $username = 'foobar';
+
+ $response1 = $this->withHeaders([
+ 'From' => 'sip:'.$password->account->identifier,
+ 'Authorization' => $this->generateDigest($password, $response0),
+ ])->json($this->method, $this->route, [
+ 'username' => $username,
+ 'algorithm' => 'SHA-256',
+ 'password' => '123456',
+ ]);
+
+ $response1
+ ->assertStatus(200)
+ ->assertJson([
+ 'id' => 2,
+ 'username' => $username
+ ]);
+ }
+}
diff --git a/flexiapi/tests/Feature/AuthenticateDigestTest.php b/flexiapi/tests/Feature/AuthenticateDigestTest.php
index 5404206..8481594 100644
--- a/flexiapi/tests/Feature/AuthenticateDigestTest.php
+++ b/flexiapi/tests/Feature/AuthenticateDigestTest.php
@@ -19,7 +19,6 @@
namespace Tests\Feature;
-use App\Helpers\Utils;
use App\Password;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -30,14 +29,13 @@ class AuthenticateDigestTest extends TestCase
{
use RefreshDatabase;
- const ROUTE = '/api/ping';
- const METHOD = 'GET';
- const ALGORITHMS = ['md5' => 'MD5', 'sha256' => 'SHA-256'];
+ protected $route = '/api/ping';
+ protected $method = 'GET';
public function testMandatoryFrom()
{
$password = factory(Password::class)->create();
- $response = $this->json(self::METHOD, self::ROUTE);
+ $response = $this->json($this->method, $this->route);
$response->assertStatus(422);
}
@@ -46,7 +44,7 @@ class AuthenticateDigestTest extends TestCase
$password = factory(Password::class)->create();
$response = $this->withHeaders([
'From' => 'sip:missing@username',
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response->assertStatus(404);
}
@@ -56,7 +54,7 @@ class AuthenticateDigestTest extends TestCase
$password = factory(Password::class)->create();
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response->assertStatus(401);
}
@@ -70,7 +68,7 @@ class AuthenticateDigestTest extends TestCase
$response = $this->withHeaders([
'From' => 'sip:'.$passwordMD5->account->identifier,
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response->assertStatus(401);
@@ -83,12 +81,12 @@ class AuthenticateDigestTest extends TestCase
$password = factory(Password::class)->create();
$response0 = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response1 = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response0),
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response1->assertStatus(200);
@@ -96,7 +94,7 @@ class AuthenticateDigestTest extends TestCase
$response2 = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response1, 'md5', '00000002'),
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response2->assertStatus(200);
@@ -104,7 +102,7 @@ class AuthenticateDigestTest extends TestCase
$response3 = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response2, 'md5', '00000002'),
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response3->assertSee('Nonce replayed');
$response3->assertStatus(401);
@@ -115,12 +113,12 @@ class AuthenticateDigestTest extends TestCase
$password = factory(Password::class)->create();
$response1 = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response2 = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response1, 'md5', '00000001'),
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response2->assertStatus(200);
@@ -130,7 +128,7 @@ class AuthenticateDigestTest extends TestCase
$response3 = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response2, 'md5', '00000002'),
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response3->assertSee('Nonce invalid');
$response3->assertStatus(401);
@@ -142,12 +140,12 @@ class AuthenticateDigestTest extends TestCase
$password = factory(Password::class)->create();
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response),
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$this->assertStringContainsString('algorithm=MD5', $response->headers->all()['www-authenticate'][0]);
@@ -159,12 +157,12 @@ class AuthenticateDigestTest extends TestCase
$password = factory(Password::class)->states('sha256')->create();
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response, 'sha256'),
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$this->assertStringContainsString('algorithm=SHA-256', $response->headers->all()['www-authenticate'][0]);
@@ -176,7 +174,7 @@ class AuthenticateDigestTest extends TestCase
$password = factory(Password::class)->states('clrtxt')->create();
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
// The server is generating all the available hash algorythms
$this->assertStringContainsString('algorithm=MD5', $response->headers->all()['www-authenticate'][0]);
@@ -192,7 +190,7 @@ class AuthenticateDigestTest extends TestCase
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response, $hash),
- ])->json(self::METHOD, self::ROUTE);
+ ])->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]);
@@ -205,63 +203,14 @@ class AuthenticateDigestTest extends TestCase
$password = factory(Password::class)->create();
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$password->password = 'wrong';
$response = $this->withHeaders([
'From' => 'sip:'.$password->account->identifier,
'Authorization' => $this->generateDigest($password, $response),
- ])->json(self::METHOD, self::ROUTE);
+ ])->json($this->method, $this->route);
$response->assertStatus(401);
}
-
- private function generateDigest(Password $password, $response, $hash = 'md5', $nc = '00000001')
- {
- $challenge = \substr($response->headers->get('www-authenticate'), 7);
- $extractedChallenge = $this->extractAuthenticateHeader($challenge);
-
- $cnonce = Utils::generateNonce();
-
- $A1 = $password->password;
- $A2 = hash($hash, self::METHOD . ':' . self::ROUTE);
- $response = hash($hash,
- sprintf(
- '%s:%s:%s:%s:%s:%s',
- $A1,
- $extractedChallenge['nonce'],
- $nc,
- $cnonce,
- $extractedChallenge['qop'],
- $A2
- )
- );
-
- $digest = \sprintf(
- 'username="%s",realm="%s",nonce="%s",nc=%s,cnonce="%s",uri="%s",qop=%s,response="%s",opaque="%s",algorithm=%s',
- $password->account->identifier,
- $extractedChallenge['realm'],
- $extractedChallenge['nonce'],
- $nc,
- $cnonce,
- self::ROUTE,
- $extractedChallenge['qop'],
- $response,
- $extractedChallenge['opaque'],
- self::ALGORITHMS[$hash],
- );
-
- return 'Digest ' . $digest;
- }
-
- private function extractAuthenticateHeader(string $string): array
- {
- preg_match_all(
- '@(realm|nonce|qop|opaque|algorithm)=[\'"]?([^\'",]+)@',
- $string,
- $array
- );
-
- return array_combine($array[1], $array[2]);
- }
}
diff --git a/flexiapi/tests/TestCase.php b/flexiapi/tests/TestCase.php
index 2932d4a..a7eca71 100644
--- a/flexiapi/tests/TestCase.php
+++ b/flexiapi/tests/TestCase.php
@@ -2,9 +2,63 @@
namespace Tests;
+use App\Password;
+use App\Helpers\Utils;
+
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
+
+ const ALGORITHMS = ['md5' => 'MD5', 'sha256' => 'SHA-256'];
+
+ protected function generateDigest(Password $password, $response, $hash = 'md5', $nc = '00000001')
+ {
+ $challenge = \substr($response->headers->get('www-authenticate'), 7);
+ $extractedChallenge = $this->extractAuthenticateHeader($challenge);
+
+ $cnonce = Utils::generateNonce();
+
+ $A1 = $password->password;
+ $A2 = hash($hash, $this->method . ':' . $this->route);
+ $response = hash($hash,
+ sprintf(
+ '%s:%s:%s:%s:%s:%s',
+ $A1,
+ $extractedChallenge['nonce'],
+ $nc,
+ $cnonce,
+ $extractedChallenge['qop'],
+ $A2
+ )
+ );
+
+ $digest = \sprintf(
+ 'username="%s",realm="%s",nonce="%s",nc=%s,cnonce="%s",uri="%s",qop=%s,response="%s",opaque="%s",algorithm=%s',
+ $password->account->identifier,
+ $extractedChallenge['realm'],
+ $extractedChallenge['nonce'],
+ $nc,
+ $cnonce,
+ $this->route,
+ $extractedChallenge['qop'],
+ $response,
+ $extractedChallenge['opaque'],
+ self::ALGORITHMS[$hash],
+ );
+
+ return 'Digest ' . $digest;
+ }
+
+ protected function extractAuthenticateHeader(string $string): array
+ {
+ preg_match_all(
+ '@(realm|nonce|qop|opaque|algorithm)=[\'"]?([^\'",]+)@',
+ $string,
+ $array
+ );
+
+ return array_combine($array[1], $array[2]);
+ }
}