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:
Timothée Jaussoin 2022-05-16 16:29:22 +02:00
parent 0221ba4587
commit 354830da7e
19 changed files with 493 additions and 68 deletions

View file

@ -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 . '

View 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));
}
}

View file

@ -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');
}
}

View file

@ -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();

View file

@ -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
*/

View file

@ -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;
}
}

View 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);
}
}

View 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
View file

@ -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.

View file

@ -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');
}
}

View file

@ -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

View file

@ -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>

View file

@ -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).

View file

@ -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

View file

@ -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');

View file

@ -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');

View file

@ -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);
}
}

View file

@ -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);
}
}