diff --git a/flexiapi/README.md b/flexiapi/README.md index cc194a7..6680881 100644 --- a/flexiapi/README.md +++ b/flexiapi/README.md @@ -105,3 +105,29 @@ To expire and/or clear old nonces a specific command should be called periodical ## Usage The `/api` page contains all the required documentation to authenticate and request the API. + +## Console commands + +FlexiAPI is shipped with several console commands that you can launch using the `artisan` executable available at the root of this project. + +### Clear Expired Nonces for DIGEST authentication + +This will remove the nonces stored that were not updated after `x minutes`. + + php artisan digest:expired-nonces-clear {minutes} + +### Remove the unconfirmed accounts + +This request will remove the accounts that were not confirmed after `x days`. In the database an unconfirmed account is having the `activated` attribute set to `false`. + + php artisan accounts:clear-unconfirmed {days} {--apply} + +The base request will not delete the related accounts by default. You need to add `--apply` to remove them. + +### Set an account admin + +This command will set the admin role to any available FlexiSIP account (the external FlexiSIP database need to be configured beforehand). You need to use the account DB id as a parameter in this command. + + php artisan accounts:set-admin {account_id} + +Once one account is declared as administrator, you can directly configure the other ones using the web panel. \ No newline at end of file diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index 29c071c..bf19dd9 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -23,6 +23,9 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Support\Str; + +use App\ApiKey; class Account extends Authenticatable { @@ -60,6 +63,11 @@ class Account extends Authenticatable return $this->hasOne('App\Admin'); } + public function apiKey() + { + return $this->hasOne('App\ApiKey'); + } + public function emailChanged() { return $this->hasOne('App\EmailChanged'); @@ -70,6 +78,16 @@ class Account extends Authenticatable return $this->attributes['username'].'@'.$this->attributes['domain']; } + public function generateApiKey() + { + $this->apiKey()->delete(); + + $apiKey = new ApiKey; + $apiKey->account_id = $this->id; + $apiKey->key = Str::random(40); + $apiKey->save(); + } + public function isAdmin() { return ($this->admin); diff --git a/flexiapi/app/ApiKey.php b/flexiapi/app/ApiKey.php new file mode 100644 index 0000000..41a9633 --- /dev/null +++ b/flexiapi/app/ApiKey.php @@ -0,0 +1,36 @@ +. +*/ + +namespace App; + +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; + +class ApiKey extends Model +{ + use HasFactory; + + protected $connection = 'local'; + protected $table = 'api_keys'; + + public function account() + { + return $this->belongsTo('App\Account'); + } +} diff --git a/flexiapi/app/Http/Controllers/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Admin/AccountController.php index 583e34b..ee8fac3 100644 --- a/flexiapi/app/Http/Controllers/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Admin/AccountController.php @@ -93,4 +93,12 @@ class AccountController extends Controller return redirect()->back(); } + + public function generateApiKey(Request $request) + { + $account = $request->user(); + $account->generateApiKey(); + + return redirect()->back(); + } } diff --git a/flexiapi/app/Http/Controllers/Api/AccountController.php b/flexiapi/app/Http/Controllers/Api/AccountController.php index e091a5b..cfff1c4 100644 --- a/flexiapi/app/Http/Controllers/Api/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/AccountController.php @@ -36,6 +36,7 @@ class AccountController extends Controller 'algorithm' => 'required|in:SHA-256,MD5', 'password' => 'required|filled', 'domain' => 'min:3', + 'activated' => 'boolean|nullable', ]); $algorithm = $request->has('password_sha256') ? 'SHA-256' : 'MD5'; @@ -43,7 +44,9 @@ class AccountController extends Controller $account = new Account; $account->username = $request->get('username'); $account->email = $request->get('email'); - $account->activated = true; + $account->activated = $request->has('activated') + ? (bool)$request->get('activated') + : false; $account->domain = $request->has('domain') ? $request->get('domain') : config('app.sip_domain'); diff --git a/flexiapi/app/Http/Kernel.php b/flexiapi/app/Http/Kernel.php index 704b630..3b59d55 100644 --- a/flexiapi/app/Http/Kernel.php +++ b/flexiapi/app/Http/Kernel.php @@ -71,7 +71,7 @@ class Kernel extends HttpKernel 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.admin' => \App\Http\Middleware\AuthenticateAdmin::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'auth.digest' => \App\Http\Middleware\AuthenticateDigest::class, + 'auth.digest_or_key' => \App\Http\Middleware\AuthenticateDigestOrKey::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, diff --git a/flexiapi/app/Http/Middleware/AuthenticateDigest.php b/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php similarity index 93% rename from flexiapi/app/Http/Middleware/AuthenticateDigest.php rename to flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php index 9cca585..6ac2275 100644 --- a/flexiapi/app/Http/Middleware/AuthenticateDigest.php +++ b/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php @@ -29,7 +29,7 @@ use Illuminate\Http\Response; use Closure; use Validator; -class AuthenticateDigest +class AuthenticateDigestOrKey { const ALGORITHMS = [ 'MD5' => 'md5', @@ -56,6 +56,21 @@ class AuthenticateDigest ->where('domain', $domain) ->firstOrFail(); + // Key authentication + if ($request->header('x-api-key')) { + if ($account->apiKey + && $account->apiKey->key == $request->header('x-api-key')) { + Auth::login($account); + $response = $next($request); + + return $response; + } + + return $this->generateUnauthorizedResponse($account); + } + + // DIGEST authentication + if ($request->header('Authorization')) { $auth = $this->extractAuthorizationHeader($request->header('Authorization')); $storedNonce = $account->nonces()->where('nonce', $auth['nonce'])->first(); diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index 538e8ff..a31fca4 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -320,27 +320,27 @@ }, { "name": "dragonmantank/cron-expression", - "version": "3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "fa4e95ff5a7f1d62c3fbc05c32729b7f3ca14b52" + "reference": "48212cdc0a79051d50d7fc2f0645c5a321caf926" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/fa4e95ff5a7f1d62c3fbc05c32729b7f3ca14b52", - "reference": "fa4e95ff5a7f1d62c3fbc05c32729b7f3ca14b52", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/48212cdc0a79051d50d7fc2f0645c5a321caf926", + "reference": "48212cdc0a79051d50d7fc2f0645c5a321caf926", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1|^8.0" }, "replace": { "mtdowling/cron-expression": "^1.0" }, "require-dev": { - "phpstan/phpstan": "^0.11", - "phpunit/phpunit": "^6.4|^7.0" + "phpstan/phpstan": "^0.11|^0.12", + "phpunit/phpunit": "^7.0|^8.0|^9.0" }, "type": "library", "autoload": { @@ -370,7 +370,7 @@ "type": "github" } ], - "time": "2020-08-21T02:30:13+00:00" + "time": "2020-10-13T01:26:01+00:00" }, { "name": "egulias/email-validator", @@ -900,16 +900,16 @@ }, { "name": "laravel/framework", - "version": "v8.9.0", + "version": "v8.10.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54" + "reference": "0c80950806cd1bc6d9a7068585a12c2bfa23bdf3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/8a6bf870bcfa1597e514a9c7ee6df44db98abb54", - "reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54", + "url": "https://api.github.com/repos/laravel/framework/zipball/0c80950806cd1bc6d9a7068585a12c2bfa23bdf3", + "reference": "0c80950806cd1bc6d9a7068585a12c2bfa23bdf3", "shasum": "" }, "require": { @@ -1059,7 +1059,7 @@ "framework", "laravel" ], - "time": "2020-10-06T14:22:36+00:00" + "time": "2020-10-13T14:20:53+00:00" }, { "name": "laravel/tinker", @@ -1195,16 +1195,16 @@ }, { "name": "league/commonmark", - "version": "1.5.5", + "version": "1.5.6", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "45832dfed6007b984c0d40addfac48d403dc6432" + "reference": "a56e91e0fa1f6d0049153a9c34f63488f6b7ce61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/45832dfed6007b984c0d40addfac48d403dc6432", - "reference": "45832dfed6007b984c0d40addfac48d403dc6432", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/a56e91e0fa1f6d0049153a9c34f63488f6b7ce61", + "reference": "a56e91e0fa1f6d0049153a9c34f63488f6b7ce61", "shasum": "" }, "require": { @@ -1286,7 +1286,7 @@ "type": "tidelift" } ], - "time": "2020-09-13T14:44:46+00:00" + "time": "2020-10-17T21:33:03+00:00" }, { "name": "league/flysystem", @@ -1435,16 +1435,16 @@ }, { "name": "league/mime-type-detection", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28" + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ea2fbfc988bade315acd5967e6d02274086d0f28", - "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa", + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa", "shasum": "" }, "require": { @@ -1482,7 +1482,7 @@ "type": "tidelift" } ], - "time": "2020-09-21T18:10:53+00:00" + "time": "2020-10-18T11:50:25+00:00" }, { "name": "monolog/monolog", @@ -1577,16 +1577,16 @@ }, { "name": "nesbot/carbon", - "version": "2.41.2", + "version": "2.41.3", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "35959c93ada06469107a05df6b15b65074a960cf" + "reference": "e148788eeae9b9b7b87996520358b86faad37b52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/35959c93ada06469107a05df6b15b65074a960cf", - "reference": "35959c93ada06469107a05df6b15b65074a960cf", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e148788eeae9b9b7b87996520358b86faad37b52", + "reference": "e148788eeae9b9b7b87996520358b86faad37b52", "shasum": "" }, "require": { @@ -1662,7 +1662,7 @@ "type": "tidelift" } ], - "time": "2020-10-10T23:35:06+00:00" + "time": "2020-10-12T20:36:09+00:00" }, { "name": "nikic/php-parser", @@ -1814,20 +1814,20 @@ }, { "name": "paragonie/random_compat", - "version": "v9.99.99", + "version": "v9.99.100", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", "shasum": "" }, "require": { - "php": "^7" + "php": ">= 7" }, "require-dev": { "phpunit/phpunit": "4.*|5.*", @@ -1855,7 +1855,7 @@ "pseudorandom", "random" ], - "time": "2018-07-02T15:55:56+00:00" + "time": "2020-10-15T08:29:30+00:00" }, { "name": "parsedown/laravel", @@ -3133,16 +3133,16 @@ }, { "name": "symfony/http-client-contracts", - "version": "v2.2.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "3a5d0fe7908daaa23e3dbf4cee3ba4bfbb19fdd3" + "reference": "41db680a15018f9c1d4b23516059633ce280ca33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/3a5d0fe7908daaa23e3dbf4cee3ba4bfbb19fdd3", - "reference": "3a5d0fe7908daaa23e3dbf4cee3ba4bfbb19fdd3", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33", "shasum": "" }, "require": { @@ -3153,8 +3153,9 @@ }, "type": "library", "extra": { + "branch-version": "2.3", "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.3-dev" }, "thanks": { "name": "symfony/contracts", @@ -3204,7 +3205,7 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2020-10-14T17:08:19+00:00" }, { "name": "symfony/http-foundation", @@ -5156,16 +5157,16 @@ }, { "name": "facade/ignition", - "version": "2.3.8", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/facade/ignition.git", - "reference": "e8fed9c382cd1d02b5606688576a35619afdf82c" + "reference": "9fc6c3d3de5271a1b94cff19dce2c9295abf0ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facade/ignition/zipball/e8fed9c382cd1d02b5606688576a35619afdf82c", - "reference": "e8fed9c382cd1d02b5606688576a35619afdf82c", + "url": "https://api.github.com/repos/facade/ignition/zipball/9fc6c3d3de5271a1b94cff19dce2c9295abf0ffa", + "reference": "9fc6c3d3de5271a1b94cff19dce2c9295abf0ffa", "shasum": "" }, "require": { @@ -5224,29 +5225,29 @@ "laravel", "page" ], - "time": "2020-10-01T23:01:14+00:00" + "time": "2020-10-14T08:59:59+00:00" }, { "name": "facade/ignition-contracts", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/facade/ignition-contracts.git", - "reference": "aeab1ce8b68b188a43e81758e750151ad7da796b" + "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/aeab1ce8b68b188a43e81758e750151ad7da796b", - "reference": "aeab1ce8b68b188a43e81758e750151ad7da796b", + "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267", + "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.3|^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.14", - "phpunit/phpunit": "^7.5|^8.0", - "vimeo/psalm": "^3.12" + "friendsofphp/php-cs-fixer": "^v2.15.8", + "phpunit/phpunit": "^9.3.11", + "vimeo/psalm": "^3.17.1" }, "type": "library", "autoload": { @@ -5273,29 +5274,29 @@ "flare", "ignition" ], - "time": "2020-07-14T10:10:28+00:00" + "time": "2020-10-16T08:27:54+00:00" }, { "name": "filp/whoops", - "version": "2.7.3", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "5d5fe9bb3d656b514d455645b3addc5f7ba7714d" + "reference": "fa50d9db1c0c2fae99cf988d27023effda5524a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/5d5fe9bb3d656b514d455645b3addc5f7ba7714d", - "reference": "5d5fe9bb3d656b514d455645b3addc5f7ba7714d", + "url": "https://api.github.com/repos/filp/whoops/zipball/fa50d9db1c0c2fae99cf988d27023effda5524a3", + "reference": "fa50d9db1c0c2fae99cf988d27023effda5524a3", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0", + "php": "^5.5.9 || ^7.0 || ^8.0", "psr/log": "^1.0.1" }, "require-dev": { "mockery/mockery": "^0.9 || ^1.0", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" }, "suggest": { @@ -5305,7 +5306,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev" + "dev-master": "2.7-dev" } }, "autoload": { @@ -5334,7 +5335,7 @@ "throwable", "whoops" ], - "time": "2020-06-14T09:00:00+00:00" + "time": "2020-10-17T09:00:00+00:00" }, { "name": "fzaninotto/faker", @@ -6350,16 +6351,16 @@ }, { "name": "scrivo/highlight.php", - "version": "v9.18.1.2", + "version": "v9.18.1.3", "source": { "type": "git", "url": "https://github.com/scrivo/highlight.php.git", - "reference": "efb6e445494a9458aa59b0af5edfa4bdcc6809d9" + "reference": "6a1699707b099081f20a488ac1f92d682181018c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/efb6e445494a9458aa59b0af5edfa4bdcc6809d9", - "reference": "efb6e445494a9458aa59b0af5edfa4bdcc6809d9", + "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/6a1699707b099081f20a488ac1f92d682181018c", + "reference": "6a1699707b099081f20a488ac1f92d682181018c", "shasum": "" }, "require": { @@ -6421,7 +6422,7 @@ "type": "github" } ], - "time": "2020-08-27T03:24:44+00:00" + "time": "2020-10-16T07:43:22+00:00" }, { "name": "sebastian/cli-parser", diff --git a/flexiapi/database/migrations/2020_10_19_085412_create_api_keys_table.php b/flexiapi/database/migrations/2020_10_19_085412_create_api_keys_table.php new file mode 100644 index 0000000..28f45a4 --- /dev/null +++ b/flexiapi/database/migrations/2020_10_19_085412_create_api_keys_table.php @@ -0,0 +1,40 @@ +. +*/ + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +class CreateApiKeysTable extends Migration +{ + public function up() + { + Schema::connection('local')->create('api_keys', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->integer('account_id')->unsigned()->unique(); + $table->string('key')->unique(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('api_keys'); + } +} diff --git a/flexiapi/resources/views/account/panel.blade.php b/flexiapi/resources/views/account/panel.blade.php index b76ec11..aa0287a 100644 --- a/flexiapi/resources/views/account/panel.blade.php +++ b/flexiapi/resources/views/account/panel.blade.php @@ -47,7 +47,7 @@ @if($account->isAdmin())
Manage the Flexisip accounts
As an administrator you can generate an API key and use it to request the different API endpoints, check the related API documentation to know how to use that key.
+ + {!! Form::open(['route' => 'admin.api_key.generate']) !!} +Restricted endpoints are protected using a DIGEST authentication mechanism.
+ +Restricted endpoints are protected using a DIGEST authentication or an API Key mechanisms.
+ +To authenticate using an API Key, you need to authenticate to your account panel and being an administrator.
+On your panel you will then find a form to generate your personnal key.
+ +You can then use your freshly generated key by adding a new x-api-key header to your API requests:
+ > GET /api/{endpoint}
+ > from: sip:foobar@sip.example.org
+ > x-api-key: {your-api-key}
+ > …
+
+To discover the available hashing algorythm you MUST send an unauthenticated request to one of the restricted endpoints.
For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the authentication layer.
password required minimum 6 charactersalgorithm required, values can be SHA-256 or MD5domain optional, the value is set to the default registration domain if not setactivated optional, a boolean, set to false by defaultTo create an account directly from the API.
This endpoint is authenticated and requires an admin account.