diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index 85f4e13..844491b 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -71,7 +71,7 @@ class Account extends Authenticatable list($usernane, $domain) = explode('@', $sip); return $query->where('username', $usernane) - ->where('domain', $domain); + ->where('domain', $domain); }; return $query->where('id', '<', 0); @@ -84,8 +84,8 @@ class Account extends Authenticatable { return $this->hasMany('App\AccountAction')->whereIn('account_id', function ($query) { $query->select('id') - ->from('accounts') - ->whereNotNull('dtmf_protocol'); + ->from('accounts') + ->whereNotNull('dtmf_protocol'); }); } @@ -124,6 +124,11 @@ class Account extends Authenticatable return $this->hasMany('App\DigestNonce'); } + public function authTokens() + { + return $this->hasMany('App\AuthToken'); + } + public function passwords() { return $this->hasMany('App\Password'); @@ -144,7 +149,7 @@ class Account extends Authenticatable */ public function getIdentifierAttribute() { - return $this->attributes['username'].'@'.$this->attributes['domain']; + return $this->attributes['username'] . '@' . $this->attributes['domain']; } public function getRealmAttribute() @@ -218,7 +223,7 @@ class Account extends Authenticatable Mail::to($this)->send(new ChangingEmail($this)); } - public function generateApiKey() + public function generateApiKey(): ApiKey { $this->apiKey()->delete(); @@ -227,6 +232,26 @@ class Account extends Authenticatable $apiKey->last_used_at = Carbon::now(); $apiKey->key = Str::random(40); $apiKey->save(); + + return $apiKey; + } + + public function generateAuthToken(): AuthToken + { + // Clean the expired and previous ones + AuthToken::where( + 'created_at', + '<', + Carbon::now()->subMinutes(AuthToken::$expirationMinutes) + )->orWhere('account_id', $this->id) + ->delete(); + + $authToken = new AuthToken; + $authToken->account_id = $this->id; + $authToken->token = Str::random(32); + $authToken->save(); + + return $authToken; } public function isAdmin() @@ -237,8 +262,8 @@ class Account extends Authenticatable public function hasTombstone() { return AccountTombstone::where('username', $this->attributes['username']) - ->where('domain', $this->attributes['domain']) - ->exists(); + ->where('domain', $this->attributes['domain']) + ->exists(); } public function updatePassword($newPassword, $algorithm) @@ -257,29 +282,29 @@ class Account extends Authenticatable $vcard = 'BEGIN:VCARD VERSION:4.0 KIND:individual -IMPP:sip:'.$this->getIdentifierAttribute(); +IMPP:sip:' . $this->getIdentifierAttribute(); if (!empty($this->attributes['display_name'])) { $vcard .= ' -FN:'.$this->attributes['display_name']; +FN:' . $this->attributes['display_name']; } else { $vcard .= ' -FN:'.$this->getIdentifierAttribute(); +FN:' . $this->getIdentifierAttribute(); } if ($this->dtmf_protocol) { $vcard .= ' -X-LINPHONE-ACCOUNT-DTMF-PROTOCOL:'.$this->dtmf_protocol; +X-LINPHONE-ACCOUNT-DTMF-PROTOCOL:' . $this->dtmf_protocol; } foreach ($this->types as $type) { $vcard .= ' -X-LINPHONE-ACCOUNT-TYPE:'.$type->key; +X-LINPHONE-ACCOUNT-TYPE:' . $type->key; } foreach ($this->actions as $action) { $vcard .= ' -X-LINPHONE-ACCOUNT-ACTION:'.$action->key.';'.$action->code; +X-LINPHONE-ACCOUNT-ACTION:' . $action->key . ';' . $action->code; } return $vcard . ' diff --git a/flexiapi/app/AuthToken.php b/flexiapi/app/AuthToken.php new file mode 100644 index 0000000..fc2bc70 --- /dev/null +++ b/flexiapi/app/AuthToken.php @@ -0,0 +1,24 @@ +belongsTo('App\Account'); + } + + public function scopeValid($query) + { + return $query->where('created_at', '>', Carbon::now()->subMinutes(self::$expirationMinutes)); + } +} diff --git a/flexiapi/app/Http/Controllers/Account/AuthTokenController.php b/flexiapi/app/Http/Controllers/Account/AuthTokenController.php new file mode 100644 index 0000000..22849fc --- /dev/null +++ b/flexiapi/app/Http/Controllers/Account/AuthTokenController.php @@ -0,0 +1,79 @@ +valid() + ->firstOrFail(); + + $result = Builder::create() + ->writer(new PngWriter()) + ->data( + $authToken->account_id + ? route('auth_tokens.auth', ['token' => $authToken->token]) + : route('auth_tokens.auth.external', ['token' => $authToken->token]) + ) + ->encoding(new Encoding('UTF-8')) + ->errorCorrectionLevel(new ErrorCorrectionLevelHigh()) + ->size(300) + ->margin(10) + ->build(); + + return response($result->getString())->header('Content-Type', $result->getMimeType()); + } + /** + * @desc Authenticate a user on a new device from a token generated from an authenticated account + */ + + public function create(Request $request) + { + $request->user()->generateAuthToken(); + + return redirect()->back(); + } + + public function auth(Request $request, string $token) + { + $authToken = AuthToken::where('token', $token)->valid()->firstOrFail(); + + Auth::login($authToken->account); + + $authToken->delete(); + + $request->session()->flash('success', 'Successfully authenticated'); + + return redirect()->route('account.panel'); + } + + /** + * @desc Assign an authenticated account to an auth token generated from an external user + */ + public function authExternal(Request $request, string $token) + { + $authToken = AuthToken::where('token', $token)->valid()->firstOrFail(); + + if (!$authToken->account_id) { + $authToken->account_id = $request->user()->id; + $authToken->save(); + + $request->session()->flash('success', 'External device successfully authenticated'); + } + + return redirect()->route('account.panel'); + } +} diff --git a/flexiapi/app/Http/Controllers/Account/AuthenticateController.php b/flexiapi/app/Http/Controllers/Account/AuthenticateController.php index 270d77a..bb9bf36 100644 --- a/flexiapi/app/Http/Controllers/Account/AuthenticateController.php +++ b/flexiapi/app/Http/Controllers/Account/AuthenticateController.php @@ -21,13 +21,13 @@ namespace App\Http\Controllers\Account; use App\Http\Controllers\Controller; use Illuminate\Http\Request; -use Illuminate\Validation\Rule; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; use Illuminate\Support\Facades\Mail; use App\Account; use App\Alias; +use App\AuthToken; use App\Helpers\Utils; use App\Libraries\OvhSMS; use App\Mail\PasswordAuthentication; @@ -247,6 +247,38 @@ class AuthenticateController extends Controller return redirect()->route('account.panel'); } + public function loginAuthToken(Request $request, ?string $token = null) + { + $authToken = null; + + if (!empty($token)) { + $authToken = AuthToken::where('token', $token)->valid()->first(); + } + + if ($authToken == null) { + $authToken = new AuthToken; + $authToken->token = Str::random(32); + $authToken->save(); + + return redirect()->route('account.authenticate.auth_token', ['token' => $authToken->token]); + } + + // If the $authToken was flashed by an authenticated user + if ($authToken->account_id) { + Auth::login($authToken->account); + + $authToken->delete(); + + $request->session()->flash('success', 'Successfully authenticated'); + + return redirect()->route('account.panel'); + } + + return view('account.authenticate.auth_token', [ + 'authToken' => $authToken + ]); + } + public function logout(Request $request) { Auth::logout(); diff --git a/flexiapi/app/Http/Controllers/Account/ProvisioningController.php b/flexiapi/app/Http/Controllers/Account/ProvisioningController.php index b69864e..b6f5986 100644 --- a/flexiapi/app/Http/Controllers/Account/ProvisioningController.php +++ b/flexiapi/app/Http/Controllers/Account/ProvisioningController.php @@ -20,6 +20,7 @@ namespace App\Http\Controllers\Account; use App\Account; +use App\AuthToken; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -51,6 +52,23 @@ class ProvisioningController extends Controller return response($result->getString())->header('Content-Type', $result->getMimeType()); } + /** + * auth_token based provisioning + */ + public function authToken(Request $request, string $token) + { + $authToken = AuthToken::where('token', $token)->valid()->firstOrFail(); + + if ($authToken->account) { + $account = $authToken->account; + $authToken->delete(); + + return $this->show($request, null, $account); + } + + abort(404); + } + /** * Authenticated provisioning */ diff --git a/flexiapi/app/Http/Controllers/Api/AccountController.php b/flexiapi/app/Http/Controllers/Api/AccountController.php index b358f4d..9c125b8 100644 --- a/flexiapi/app/Http/Controllers/Api/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/AccountController.php @@ -23,7 +23,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\Rule; use Illuminate\Support\Str; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Cookie; use App\Http\Controllers\Controller; use Carbon\Carbon; @@ -176,15 +175,4 @@ class AccountController extends Controller return Account::where('id', $request->user()->id) ->delete(); } - - public function generateApiKey(Request $request) - { - $account = $request->user(); - $account->generateApiKey(); - - $account->refresh(); - Cookie::queue('x-api-key', $account->apiKey->key, config('app.api_key_expiration_minutes')); - - return $account->apiKey->key; - } } diff --git a/flexiapi/app/Http/Controllers/Api/ApiKeyController.php b/flexiapi/app/Http/Controllers/Api/ApiKeyController.php new file mode 100644 index 0000000..7279f76 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/ApiKeyController.php @@ -0,0 +1,42 @@ +user(); + $account->generateApiKey(); + + $account->refresh(); + Cookie::queue('x-api-key', $account->apiKey->key, config('app.api_key_expiration_minutes')); + + return $account->apiKey->key; + } + + public function generateFromToken(string $token) + { + $authToken = AuthToken::where('token', $token)->valid()->firstOrFail(); + + if ($authToken->account) { + $authToken->account->generateApiKey(); + + $authToken->account->refresh(); + Cookie::queue('x-api-key', $authToken->account->apiKey->key, config('app.api_key_expiration_minutes')); + + $apiKey = $authToken->account->apiKey->key; + $authToken->delete(); + + return response()->json(['api_key' => $apiKey]); + } + + abort(404); + } +} diff --git a/flexiapi/app/Http/Controllers/Api/AuthTokenController.php b/flexiapi/app/Http/Controllers/Api/AuthTokenController.php new file mode 100644 index 0000000..e34190d --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/AuthTokenController.php @@ -0,0 +1,34 @@ +token = Str::random(32); + $authToken->save(); + + return $authToken; + } + + public function attach(Request $request, string $token) + { + $authToken = AuthToken::where('token', $token)->valid()->firstOrFail(); + + if (!$authToken->account_id) { + $authToken->account_id = $request->user()->id; + $authToken->save(); + + return; + } + + abort(404); + } +} diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index b8e49bf..ad35bcc 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -1468,16 +1468,16 @@ }, { "name": "laravel/framework", - "version": "v8.83.16", + "version": "v8.83.17", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "6be5abd144faf517879af7298e9d79f06f250f75" + "reference": "2cf142cd5100b02da248acad3988bdaba5635e16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/6be5abd144faf517879af7298e9d79f06f250f75", - "reference": "6be5abd144faf517879af7298e9d79f06f250f75", + "url": "https://api.github.com/repos/laravel/framework/zipball/2cf142cd5100b02da248acad3988bdaba5635e16", + "reference": "2cf142cd5100b02da248acad3988bdaba5635e16", "shasum": "" }, "require": { @@ -1637,7 +1637,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-06-07T15:09:06+00:00" + "time": "2022-06-21T14:38:31+00:00" }, { "name": "laravel/serializable-closure", @@ -3934,16 +3934,16 @@ }, { "name": "symfony/console", - "version": "v5.4.9", + "version": "v5.4.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb" + "reference": "4d671ab4ddac94ee439ea73649c69d9d200b5000" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/829d5d1bf60b2efeb0887b7436873becc71a45eb", - "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb", + "url": "https://api.github.com/repos/symfony/console/zipball/4d671ab4ddac94ee439ea73649c69d9d200b5000", + "reference": "4d671ab4ddac94ee439ea73649c69d9d200b5000", "shasum": "" }, "require": { @@ -4013,7 +4013,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.9" + "source": "https://github.com/symfony/console/tree/v5.4.10" }, "funding": [ { @@ -4029,7 +4029,7 @@ "type": "tidelift" } ], - "time": "2022-05-18T06:17:34+00:00" + "time": "2022-06-26T13:00:04+00:00" }, { "name": "symfony/css-selector", @@ -4464,16 +4464,16 @@ }, { "name": "symfony/http-foundation", - "version": "v5.4.9", + "version": "v5.4.10", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6b0d0e4aca38d57605dcd11e2416994b38774522" + "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6b0d0e4aca38d57605dcd11e2416994b38774522", - "reference": "6b0d0e4aca38d57605dcd11e2416994b38774522", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e7793b7906f72a8cc51054fbca9dcff7a8af1c1e", + "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e", "shasum": "" }, "require": { @@ -4517,7 +4517,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.9" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.10" }, "funding": [ { @@ -4533,20 +4533,20 @@ "type": "tidelift" } ], - "time": "2022-05-17T15:07:29+00:00" + "time": "2022-06-19T13:13:40+00:00" }, { "name": "symfony/http-kernel", - "version": "v5.4.9", + "version": "v5.4.10", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "34b121ad3dc761f35fe1346d2f15618f8cbf77f8" + "reference": "255ae3b0a488d78fbb34da23d3e0c059874b5948" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/34b121ad3dc761f35fe1346d2f15618f8cbf77f8", - "reference": "34b121ad3dc761f35fe1346d2f15618f8cbf77f8", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/255ae3b0a488d78fbb34da23d3e0c059874b5948", + "reference": "255ae3b0a488d78fbb34da23d3e0c059874b5948", "shasum": "" }, "require": { @@ -4629,7 +4629,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.9" + "source": "https://github.com/symfony/http-kernel/tree/v5.4.10" }, "funding": [ { @@ -4645,20 +4645,20 @@ "type": "tidelift" } ], - "time": "2022-05-27T07:09:08+00:00" + "time": "2022-06-26T16:57:59+00:00" }, { "name": "symfony/mime", - "version": "v5.4.9", + "version": "v5.4.10", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2b3802a24e48d0cfccf885173d2aac91e73df92e" + "reference": "02265e1e5111c3cd7480387af25e82378b7ab9cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2b3802a24e48d0cfccf885173d2aac91e73df92e", - "reference": "2b3802a24e48d0cfccf885173d2aac91e73df92e", + "url": "https://api.github.com/repos/symfony/mime/zipball/02265e1e5111c3cd7480387af25e82378b7ab9cc", + "reference": "02265e1e5111c3cd7480387af25e82378b7ab9cc", "shasum": "" }, "require": { @@ -4712,7 +4712,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.9" + "source": "https://github.com/symfony/mime/tree/v5.4.10" }, "funding": [ { @@ -4728,7 +4728,7 @@ "type": "tidelift" } ], - "time": "2022-05-21T10:24:18+00:00" + "time": "2022-06-09T12:22:40+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5784,16 +5784,16 @@ }, { "name": "symfony/string", - "version": "v5.4.9", + "version": "v5.4.10", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99" + "reference": "4432bc7df82a554b3e413a8570ce2fea90e94097" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", - "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", + "url": "https://api.github.com/repos/symfony/string/zipball/4432bc7df82a554b3e413a8570ce2fea90e94097", + "reference": "4432bc7df82a554b3e413a8570ce2fea90e94097", "shasum": "" }, "require": { @@ -5850,7 +5850,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.9" + "source": "https://github.com/symfony/string/tree/v5.4.10" }, "funding": [ { @@ -5866,7 +5866,7 @@ "type": "tidelift" } ], - "time": "2022-04-19T10:40:37+00:00" + "time": "2022-06-26T15:57:47+00:00" }, { "name": "symfony/translation", @@ -9065,5 +9065,5 @@ "platform-overrides": { "php": "7.3" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/flexiapi/composer.phar b/flexiapi/composer.phar index ae0ea7b..6d812d6 100755 Binary files a/flexiapi/composer.phar and b/flexiapi/composer.phar differ diff --git a/flexiapi/database/migrations/2022_05_16_123726_create_auth_tokens_table.php b/flexiapi/database/migrations/2022_05_16_123726_create_auth_tokens_table.php new file mode 100644 index 0000000..7984748 --- /dev/null +++ b/flexiapi/database/migrations/2022_05_16_123726_create_auth_tokens_table.php @@ -0,0 +1,28 @@ +id(); + + $table->integer('account_id')->unsigned()->nullable(); + $table->string('token', 32); + + $table->foreign('account_id')->references('id') + ->on('accounts')->onDelete('cascade'); + + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('auth_tokens'); + } +} diff --git a/flexiapi/resources/views/account/authenticate/auth_token.blade.php b/flexiapi/resources/views/account/authenticate/auth_token.blade.php new file mode 100644 index 0000000..b36e44d --- /dev/null +++ b/flexiapi/resources/views/account/authenticate/auth_token.blade.php @@ -0,0 +1,13 @@ +@extends('layouts.main') + +@section('content') + @if (Auth::check()) + @include('parts.already_auth') + @else +
Scan the following QR Code using an authenticated device and wait a few seconds.
+You can automatically authenticate another device on this panel by flashing the following QR Code. +Once generated the QR Code stays valid for a few minutes.
+ +@foreach ($account->authTokens()->valid()->get() as $authToken) +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.
diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index aa2ef36..2561321 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -98,6 +98,18 @@ JSON parameters: Create and return an `account_creation_token`. +## Auth Tokens + +### `POST /accounts/auth_token` +Public +Generate an `auth_token`. To attach the generated token to an account see [`auth_token` attachement endpoint](#get-accountsauthtokenauthtokenattach). + +#### `GET /accounts/auth_token/{auth_token}/attach` +User +Attach a publicly generated authentication token to the currently authenticated account. + +Return `404` if the token is non existing or invalid. + ## Accounts ### `POST /accounts/with-account-creation-token` @@ -135,6 +147,14 @@ JSON parameters: * `code` the PIN code +### `GET /accounts/me/api_key/{auth_token}` +Public +Generate and retrieve a fresh API Key from an `auth_token`. The `auth_token` must be attached to an existing account, see [`auth_token` attachement endpoint](#get-accountsauthtokenauthtokenattach) to do so. + +Return `404` if the token is invalid or not attached. + +This endpoint is also setting the API Key as a Cookie. + ### `GET /accounts/me/api_key` User Generate and retrieve a fresh API Key. @@ -251,7 +271,7 @@ Return a user contact. ## Contacts -### `GET /accounts/{id}/contacts/` +### `GET /accounts/{id}/contacts` Admin Get all the account contacts. @@ -267,7 +287,7 @@ Remove a contact from the list. The following endpoints will return `403 Forbidden` if the requested account doesn't have a DTMF protocol configured. -### `GET /accounts/{id}/actions/` +### `GET /accounts/{id}/actions` Admin Show an account related actions. @@ -299,7 +319,7 @@ Delete an account related action. ## Account Types -### `GET /account_types/` +### `GET /account_types` Admin Show all the account types. @@ -307,7 +327,7 @@ Show all the account types. Admin Show an account type. -### `POST /account_types/` +### `POST /account_types` Admin Create an account type. @@ -368,7 +388,7 @@ The following URLs are **not API endpoints** they are not returning `JSON` conte When an account is having an available `provisioning_token` it can be provisioned using the two following URL. -### `GET /provisioning/` +### `GET /provisioning` Public Return the provisioning information available in the liblinphone configuration file (if correctly configured). diff --git a/flexiapi/resources/views/parts/password_recovery.blade.php b/flexiapi/resources/views/parts/password_recovery.blade.php index 6b567bc..ab6907f 100644 --- a/flexiapi/resources/views/parts/password_recovery.blade.php +++ b/flexiapi/resources/views/parts/password_recovery.blade.php @@ -5,4 +5,7 @@ or your Phone number @endif ++ …or login using an already authenticated device by flashing a QRcode. +
@endif \ No newline at end of file diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index 3895b35..c0dbb8d 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -36,12 +36,18 @@ Route::post('accounts/with-token', 'Api\AccountController@store'); Route::post('accounts/{sip}/activate/email', 'Api\AccountController@activateEmail'); Route::post('accounts/{sip}/activate/phone', 'Api\AccountController@activatePhone'); +Route::post('accounts/auth_token', 'Api\AuthTokenController@store'); + +Route::get('accounts/me/api_key/{auth_token}', 'Api\ApiKeyController@generateFromToken')->middleware('cookie', 'cookie.encrypt'); + Route::group(['middleware' => ['auth.digest_or_key']], function () { Route::get('statistic/month', 'Api\StatisticController@month'); Route::get('statistic/week', 'Api\StatisticController@week'); Route::get('statistic/day', 'Api\StatisticController@day'); - Route::get('accounts/me/api_key', 'Api\AccountController@generateApiKey')->middleware('cookie', 'cookie.encrypt'); + Route::get('accounts/auth_token/{auth_token}/attach', 'Api\AuthTokenController@attach'); + + Route::get('accounts/me/api_key', 'Api\ApiKeyController@generate')->middleware('cookie', 'cookie.encrypt'); Route::get('accounts/me', 'Api\AccountController@show'); Route::delete('accounts/me', 'Api\AccountController@delete'); diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index 1ab68db..349c6e2 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -32,6 +32,8 @@ if (config('app.web_panel')) { Route::get('login/phone', 'Account\AuthenticateController@loginPhone')->name('account.login_phone'); Route::post('authenticate/phone', 'Account\AuthenticateController@authenticatePhone')->name('account.authenticate.phone'); Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@validatePhone')->name('account.authenticate.phone_confirm'); + + Route::get('authenticate/qrcode/{token?}', 'Account\AuthenticateController@loginAuthToken')->name('account.authenticate.auth_token'); } Route::group(['middleware' => 'auth.digest_or_key'], function () { @@ -42,6 +44,7 @@ Route::group(['middleware' => 'auth.digest_or_key'], function () { Route::get('contacts/vcard', 'Account\ContactVcardController@index')->name('account.contacts.vcard.index'); }); +Route::get('provisioning/auth_token/{auth_token}', 'Account\ProvisioningController@authToken')->name('provisioning.auth_token'); Route::get('provisioning/qrcode/{provisioning_token}', 'Account\ProvisioningController@qrcode')->name('provisioning.qrcode'); Route::get('provisioning/{provisioning_token?}', 'Account\ProvisioningController@show')->name('provisioning.show'); @@ -75,8 +78,15 @@ if (config('app.web_panel')) { Route::get('devices', 'Account\DeviceController@index')->name('account.device.index'); Route::get('devices/delete/{id}', 'Account\DeviceController@delete')->name('account.device.delete'); Route::delete('devices/{id}', 'Account\DeviceController@destroy')->name('account.device.destroy'); + + Route::post('auth_tokens', 'Account\AuthTokenController@create')->name('account.auth_tokens.create'); + + Route::get('auth_tokens/auth/external/{token}', 'Account\AuthTokenController@authExternal')->name('auth_tokens.auth.external'); }); + Route::get('auth_tokens/qrcode/{token}', 'Account\AuthTokenController@qrcode')->name('auth_tokens.qrcode'); + Route::get('auth_tokens/auth/{token}', 'Account\AuthTokenController@auth')->name('auth_tokens.auth'); + Route::group(['middleware' => 'auth.admin'], function () { // Statistics Route::get('admin/statistics/day', 'Admin\StatisticsController@showDay')->name('admin.statistics.show.day'); diff --git a/flexiapi/tests/Feature/AccountApiKeyTest.php b/flexiapi/tests/Feature/AccountApiKeyTest.php index 96be79b..a03600f 100644 --- a/flexiapi/tests/Feature/AccountApiKeyTest.php +++ b/flexiapi/tests/Feature/AccountApiKeyTest.php @@ -56,4 +56,60 @@ class AccountApiKeyTest extends TestCase ->assertSee($password->account->apiKey->key) ->assertPlainCookie('x-api-key', $password->account->apiKey->key); } + + public function testAuthToken() + { + // Generate a public auth_token + $response = $this->json('POST', '/api/accounts/auth_token') + ->assertStatus(201) + ->assertJson([ + 'token' => true + ])->content(); + + $authToken = json_decode($response)->token; + + // Attach the auth_token to the account + $password = Password::factory()->create(); + $password->account->generateApiKey(); + + $this->keyAuthenticated($password->account) + ->json($this->method, '/api/accounts/auth_token/' . $authToken . '/attach') + ->assertStatus(200); + + // Re-attach + $this->keyAuthenticated($password->account) + ->json($this->method, '/api/accounts/auth_token/' . $authToken . '/attach') + ->assertStatus(404); + + // Attach using a wrong auth_token + $this->keyAuthenticated($password->account) + ->json($this->method, '/api/accounts/auth_token/wrong_token/attach') + ->assertStatus(404); + + // Retrieve an API key from the attached auth_token + $response = $this->json($this->method, $this->route . '/' . $authToken) + ->assertStatus(200) + ->assertJson([ + 'api_key' => true + ])->content(); + + $apiKey = json_decode($response)->api_key; + + // Re-retrieve + $this->json($this->method, $this->route . '/' . $authToken) + ->assertStatus(404); + + // Check the if the API key can be used for the account + + $response = $this->withHeaders([ + 'From' => 'sip:'.$password->account->identifier, + 'x-api-key' => $apiKey, + ]) + ->json($this->method, '/api/accounts/me') + ->assertStatus(200) + ->content(); + + // Check if the account was correctly attached + $this->assertEquals(json_decode($response)->email, $password->account->email); + } } \ No newline at end of file diff --git a/flexiapi/tests/Feature/AccountProvisioningTest.php b/flexiapi/tests/Feature/AccountProvisioningTest.php index a8f5688..2d76e9a 100644 --- a/flexiapi/tests/Feature/AccountProvisioningTest.php +++ b/flexiapi/tests/Feature/AccountProvisioningTest.php @@ -25,6 +25,7 @@ use Tests\TestCase; use App\Password; use App\Admin; use App\Account as DBAccount; +use App\AuthToken; class AccountProvisioningTest extends TestCase { @@ -121,4 +122,37 @@ class AccountProvisioningTest extends TestCase ->assertHeader('Content-Type', 'application/xml') ->assertSee('ha1'); } + + public function testAuthTokenProvisioning() + { + // Generate a public auth_token and attach it + $response = $this->json('POST', '/api/accounts/auth_token') + ->assertStatus(201) + ->assertJson([ + 'token' => true + ])->content(); + + $authToken = json_decode($response)->token; + + $password = Password::factory()->create(); + $password->account->generateApiKey(); + + $this->keyAuthenticated($password->account) + ->json($this->method, '/api/accounts/auth_token/' . $authToken . '/attach') + ->assertStatus(200); + + // Use the auth_token to provision the account + $this->assertEquals(AuthToken::count(), 1); + + $this->get($this->route.'/auth_token/'.$authToken) + ->assertStatus(200) + ->assertHeader('Content-Type', 'application/xml') + ->assertSee('ha1'); + + $this->assertEquals(AuthToken::count(), 0); + + // Try to re-use the auth_token + $this->get($this->route.'/auth_token/'.$authToken) + ->assertStatus(404); + } } \ No newline at end of file