Fix FLEXIAPI-152 API Key usage clarification

This commit is contained in:
Timothée Jaussoin 2024-04-09 09:10:08 +00:00
parent 42e7ed83c0
commit 7418d79b41
12 changed files with 114 additions and 21 deletions

View file

@ -7,6 +7,7 @@ v1.5
- Fix FLEXIAPI-156 Disable the Phone change web form when PHONE_AUTHENTICATION is disabled
- Fix FLEXIAPI-155 Add a new accountServiceAccountUpdatedHook and accountServiceAccountDeletedHook
- Fix FLEXIAPI-153 Add phone and email to be changed in the Activity panel
- Fix FLEXIAPI-152 API Key usage clarification
- Fix FLEXIAPI-151 Migrate to hCaptcha
- Fix FLEXIAPI-150 Use the same account_id parameter for both API and Web routes
- Fix FLEXIAPI-149 Add a toggle to disable phone check on username for admin endpoints and forms

View file

@ -356,7 +356,7 @@ class Account extends Authenticatable
return ($this->activationExpiration && $this->activationExpiration->isExpired());
}
public function generateApiKey(): ApiKey
public function generateApiKey(?Request $request = null): ApiKey
{
$this->apiKey()->delete();
@ -364,6 +364,7 @@ class Account extends Authenticatable
$apiKey->account_id = $this->id;
$apiKey->last_used_at = Carbon::now();
$apiKey->key = Str::random(40);
$apiKey->ip = $request ? $request->ip() : '127.0.0.1';
$apiKey->save();
return $apiKey;

View file

@ -83,12 +83,7 @@ class CreateAdminAccount extends Command
$account->created_at = Carbon::now()->subYears(3);
$account->save();
$apiKey = new ApiKey;
$apiKey->account_id = $account->id;
$apiKey->last_used_at = Carbon::now();
$apiKey->key = Str::random(10);
$apiKey->save();
$account->generateApiKey();
$account->updatePassword($password);
$this->info('Admin test account created: "' . $username . '@' . $domain . '" | Password: "' . $password . '" | API Key: "' . $apiKey->key . '"');

View file

@ -34,7 +34,7 @@ class ApiKeyController extends Controller
public function update(Request $request)
{
$account = $request->user();
$account->generateApiKey();
$account->generateApiKey($request);
return redirect()->back();
}

View file

@ -29,7 +29,7 @@ class ApiKeyController extends Controller
public function generate(Request $request)
{
$account = $request->user();
$account->generateApiKey();
$account->generateApiKey($request);
$account->refresh();
Cookie::queue('x-api-key', $account->apiKey->key, config('app.api_key_expiration_minutes'));
@ -37,12 +37,12 @@ class ApiKeyController extends Controller
return $account->apiKey->key;
}
public function generateFromToken(string $token)
public function generateFromToken(Request $request, string $token)
{
$authToken = AuthToken::where('token', $token)->valid()->firstOrFail();
if ($authToken->account) {
$authToken->account->generateApiKey();
$authToken->account->generateApiKey($request);
$authToken->account->refresh();
Cookie::queue('x-api-key', $authToken->account->apiKey->key, config('app.api_key_expiration_minutes'));

View file

@ -26,6 +26,7 @@ use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Response;
use Closure;
use Illuminate\Http\Request;
use Validator;
class AuthenticateDigestOrKey
@ -37,7 +38,7 @@ class AuthenticateDigestOrKey
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
public function handle(Request $request, Closure $next)
{
if ($request->bearerToken() && Auth::check()) {
return $next($request);
@ -50,8 +51,9 @@ class AuthenticateDigestOrKey
$query->withoutGlobalScopes();
}])->where('key', $request->header('x-api-key') ?? $request->cookie('x-api-key'))->first();
if ($apiKey) {
if ($apiKey && ($apiKey->ip == null || $apiKey->ip == $request->ip())) {
$apiKey->last_used_at = Carbon::now();
$apiKey->requests = $apiKey->requests + 1;
$apiKey->save();
Auth::login($apiKey->account);

View file

@ -0,0 +1,25 @@
<?php
use App\ApiKey;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->string('ip')->nullable();
$table->integer('requests')->default(0);
});
}
public function down()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('ip');
$table->dropColumn('requests');
});
}
};

View file

@ -116,7 +116,7 @@ pre {
color: var(--second-7);
}
b {
b, strong {
font-weight: bold;
}

View file

@ -7,7 +7,6 @@
@endsection
@section('content')
<div class="large">
<h2><i class="material-symbols-outlined">key</i>API Key</h2>
@ -16,15 +15,21 @@
<p>An unused key will expires after some times.</p>
@if ($account->apiKey)
<h3>Current Api Key</h3>
<form>
<div>
<input type="text" readonly value="{{ $account->apiKey->key }}">
<label>Key</label>
<small>Can only be used from the following ip: {{ $account->apiKey->ip }} | {{ $account->apiKey->requests }} requests</small>
</div>
</form>
@endif
<form method="POST" action="{{ route('account.api_key.update') }}" accept-charset="UTF-8">
@csrf
<div>
<input readonly placeholder="No key yet, press Generate"
@if ($account->apiKey) value="{{ $account->apiKey->key }}" @endif>
<label>Key</label>
</div>
<div>
<button type="submit" class="btn btn-primary">Generate</button>
<button type="submit" class="btn btn-primary">@if ($account->apiKey)Refresh the current key @else Generate a new key @endif</button>
</div>
</form>
</div>

View file

@ -14,6 +14,40 @@
@include('admin.account.parts.tabs')
@if ($account->apiKey)
<h3>Api Key</h3>
<table class="third">
<thead>
<tr>
<th>Code</th>
<th>Created</th>
<th>Last usage</th>
<th>IP</th>
<th>Requests</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{{ $account->apiKey->key }}
</td>
<td>
{{ $account->apiKey->created_at }}
</td>
<td>
{{ $account->apiKey->last_used_at }}
</td>
<td>
{{ $account->apiKey->ip ?? '-' }}
</td>
<td>
{{ $account->apiKey->requests }}
</td>
</tr>
</tbody>
</table>
@endif
@if ($account->accountCreationToken)
<h3>Account Creation Token</h3>
<table class="third">

View file

@ -29,6 +29,8 @@ The endpoints are accessible using three different models:
You can retrieve an API Key from @if (config('app.web_panel')) [your account panel]({{ route('account.login') }}) @else your account panel @endif or using <a href="#get-accountsmeapikey">the dedicated API endpoint</a>.
**The generated API Key will be restricted to the IP that generates it and will be destroyed if not used after some times.**
You can then use your freshly generated key by adding a new `x-api-key` header to your API requests:
```

View file

@ -19,7 +19,10 @@
namespace Tests\Feature;
use App\Account;
use App\ApiKey;
use App\Password;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ApiAccountApiKeyTest extends TestCase
@ -53,6 +56,31 @@ class ApiAccountApiKeyTest extends TestCase
->assertPlainCookie('x-api-key', $password->account->apiKey->key);
}
public function testRequest()
{
$account = Account::factory()->create();
$account->generateApiKey();
$this->keyAuthenticated($account)
->json($this->method, '/api/accounts/me')
->assertStatus(200);
$this->keyAuthenticated($account)
->json($this->method, '/api/accounts/me')
->assertStatus(200);
$this->assertDatabaseHas('api_keys', [
'account_id' => $account->id,
'requests' => 2
]);
DB::table('api_keys')->update(['ip' => 'no_localhost']);
$this->keyAuthenticated($account)
->json($this->method, '/api/accounts/me')
->assertStatus(401);
}
public function testAuthToken()
{
// Generate a public auth_token