mirror of
https://gitlab.linphone.org/BC/public/flexisip-account-manager.git
synced 2026-01-17 10:08:05 +00:00
QRCode based authentication
Add routes, model and controller for AuthToken Create auth_tokens table Allow auth_token to be used for provisioning Reorganize the API Update the dependencies
This commit is contained in:
parent
0221ba4587
commit
354830da7e
19 changed files with 493 additions and 68 deletions
|
|
@ -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 . '
|
||||
|
|
|
|||
24
flexiapi/app/AuthToken.php
Normal file
24
flexiapi/app/AuthToken.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AuthToken extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public static $expirationMinutes = 5;
|
||||
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo('App\Account');
|
||||
}
|
||||
|
||||
public function scopeValid($query)
|
||||
{
|
||||
return $query->where('created_at', '>', Carbon::now()->subMinutes(self::$expirationMinutes));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\AuthToken;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Encoding\Encoding;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthTokenController extends Controller
|
||||
{
|
||||
public function qrcode(string $token)
|
||||
{
|
||||
$authToken = AuthToken::where('token', $token)
|
||||
->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
flexiapi/app/Http/Controllers/Api/ApiKeyController.php
Normal file
42
flexiapi/app/Http/Controllers/Api/ApiKeyController.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\AuthToken;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
|
||||
class ApiKeyController extends Controller
|
||||
{
|
||||
public function generate(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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
34
flexiapi/app/Http/Controllers/Api/AuthTokenController.php
Normal file
34
flexiapi/app/Http/Controllers/Api/AuthTokenController.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\AuthToken;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AuthTokenController extends Controller
|
||||
{
|
||||
public function store()
|
||||
{
|
||||
$authToken = new AuthToken;
|
||||
$authToken->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);
|
||||
}
|
||||
}
|
||||
72
flexiapi/composer.lock
generated
72
flexiapi/composer.lock
generated
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateAuthTokensTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('auth_tokens', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
@extends('layouts.main')
|
||||
|
||||
@section('content')
|
||||
@if (Auth::check())
|
||||
@include('parts.already_auth')
|
||||
@else
|
||||
<p class="text-center pt-3">Scan the following QR Code using an authenticated device and wait a few seconds.</p>
|
||||
<p class="text-center pt-3"><img src="{{ route('auth_tokens.qrcode', ['token' => $authToken->token]) }}"></p>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function () { location.reload(1); }, 5000);
|
||||
</script>
|
||||
@endif
|
||||
@endsection
|
||||
|
|
@ -84,6 +84,19 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
<h3 class="mt-3">Automatic authentication</h3>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
@foreach ($account->authTokens()->valid()->get() as $authToken)
|
||||
<img src="{{ route('auth_tokens.qrcode', ['token' => $authToken->token]) }}">
|
||||
@endforeach
|
||||
|
||||
{!! Form::open(['route' => 'account.auth_tokens.create']) !!}
|
||||
<button type="submit" class="btn btn-primary">Generate</button>
|
||||
{!! Form::close() !!}
|
||||
|
||||
<h3 class="mt-3">API Key</h3>
|
||||
|
||||
<p>You can generate an API key and use it to request the different API endpoints, <a href="{{ route('api') }}">check the related API documentation</a> to know how to use that key.</p>
|
||||
|
|
|
|||
|
|
@ -98,6 +98,18 @@ JSON parameters:
|
|||
|
||||
Create and return an `account_creation_token`.
|
||||
|
||||
## Auth Tokens
|
||||
|
||||
### `POST /accounts/auth_token`
|
||||
<span class="badge badge-success">Public</span>
|
||||
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`
|
||||
<span class="badge badge-info">User</span>
|
||||
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}`
|
||||
<span class="badge badge-info">Public</span>
|
||||
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`
|
||||
<span class="badge badge-info">User</span>
|
||||
Generate and retrieve a fresh API Key.
|
||||
|
|
@ -251,7 +271,7 @@ Return a user contact.
|
|||
|
||||
## Contacts
|
||||
|
||||
### `GET /accounts/{id}/contacts/`
|
||||
### `GET /accounts/{id}/contacts`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
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`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
Show an account related actions.
|
||||
|
||||
|
|
@ -299,7 +319,7 @@ Delete an account related action.
|
|||
|
||||
## Account Types
|
||||
|
||||
### `GET /account_types/`
|
||||
### `GET /account_types`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
Show all the account types.
|
||||
|
||||
|
|
@ -307,7 +327,7 @@ Show all the account types.
|
|||
<span class="badge badge-warning">Admin</span>
|
||||
Show an account type.
|
||||
|
||||
### `POST /account_types/`
|
||||
### `POST /account_types`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
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`
|
||||
<span class="badge badge-success">Public</span>
|
||||
Return the provisioning information available in the liblinphone configuration file (if correctly configured).
|
||||
|
||||
|
|
|
|||
|
|
@ -5,4 +5,7 @@
|
|||
or your <a href="{{ route('account.login_phone') }}">Phone number</a>
|
||||
@endif
|
||||
</p>
|
||||
<p class="text-center">
|
||||
…or login using an already authenticated device <a href="{{ route('account.authenticate.auth_token') }}">by flashing a QRcode</a>.
|
||||
</p>
|
||||
@endif
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue