Implement & test API-authentication, simplify Api-routes

This commit is contained in:
Ralph J. Smit
2022-07-01 12:00:16 +02:00
parent 28ffc8e240
commit 34b838c259
8 changed files with 67 additions and 41 deletions

View File

@@ -56,7 +56,8 @@ class Handler extends ExceptionHandler
{
$response = parent::render($request, $exception);
if (in_array($response->status(), [403, 404])) {
// Only return an Inertia-response when there are special Vue-templates (403 and 404) and when it isn't an API request.
if (in_array($response->status(), [403, 404]) && ! $request->routeIs('api.*')) {
return app(HandleInertiaRequests::class)
->handle($request, fn () => inertia()->render('Errors/' . $response->status(), ['status' => $response->status()])
->toResponse($request));

View File

@@ -1,17 +1,19 @@
<?php
use App\Models\Setting;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Arr;
if (!function_exists('setting')) {
/**
* @param null $key
* @param null $default
* @return array|ArrayAccess|bool|\Illuminate\Contracts\Foundation\Application|mixed
* @return array|ArrayAccess|bool|Application|mixed
*/
function setting($key = null, $default = null)
{
if (is_array($key)) {
\App\Models\Setting::updateOrCreate([
Setting::updateOrCreate([
'key' => key($key)
], [
'value' => Arr::first($key)
@@ -20,6 +22,7 @@ if (!function_exists('setting')) {
try {
cache()->forget('core.settings');
} catch (Exception $e) {
//
}
return true;

View File

@@ -8,28 +8,27 @@ use App\Services\Ploi\Exceptions\Http\Unauthenticated;
class GlobalApiAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
public function handle(Request $request, Closure $next): mixed
{
if (!$this->isAuthenticated($request)) {
abort_unless($this->hasApiEnabled(), 404);
abort_unless($this->isAuthenticated($request), 403);
if (! $this->isAuthenticated($request)) {
throw new Unauthenticated('Unauthenticated for global access.');
}
return $next($request);
}
protected function hasApiEnabled(): bool
{
return setting('enable_api') && (bool) setting('api_token');
}
protected function isAuthenticated(Request $request)
{
return
setting('enable_api') &&
setting('api_token') &&
$request->bearerToken() &&
$request->bearerToken() === decrypt(setting('api_token'));
return $request->bearerToken()
&& $request->bearerToken() === decrypt(setting('api_token'));
}
}

View File

@@ -6,13 +6,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
public function toArray($request): array
{
return [
'id' => $this->id,

View File

@@ -3,6 +3,7 @@
namespace App\Providers;
use App\Models\Setting;
use Exception;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
@@ -14,7 +15,7 @@ class AppServiceProvider extends ServiceProvider
return $app['cache']->remember('core.settings', now()->addDay(), function () {
try {
return Setting::pluck('value', 'key')->toArray();
} catch (\Exception $exception) {
} catch (Exception $exception) {
return [];
}
});

View File

@@ -2,11 +2,11 @@
namespace App\Providers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
@@ -36,12 +36,13 @@ class RouteServiceProvider extends ServiceProvider
$this->configureRateLimiting();
$this->routes(function () {
if (setting('enable_api')) {
// The settings('enable_api') is now handled by the GlobalApiAuthenticated middleware,
// because the conditional inside this service makes testing very hard.
Route::prefix('api')
->middleware('api')
->middleware(['api', 'global.api.authenticated'])
->namespace($this->namespace . '\Api')
->as('api.')
->group(base_path('routes/api.php'));
}
Route::middleware('web')
->namespace($this->namespace)

View File

@@ -1,11 +1,8 @@
<?php
use App\Http\Controllers\Api\UserController;
use Illuminate\Support\Facades\Route;
Route::group(['middleware' => 'global.api.authenticated'], function () {
Route::group(['prefix' => 'users'], function () {
Route::get('/', 'UserController@index');
Route::post('/', 'UserController@store');
Route::get('{user}', 'UserController@show');
});
});
Route::resource('users', UserController::class)
->names('user')
->only('index', 'store', 'show');

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Support\Facades\App;
use function Pest\Laravel\get;
it('cannot use the API when a user doesn\'t have the API enabled', function () {
expect(setting('enable_api'))->toBeNull();
expect(setting('api_token'))->toBeNull();
get(route('api.user.index'))
->assertNotFound();
setting(['enable_api' => true,]);
setting(['api_token' => encrypt('secret')]);
App::forgetInstance('settings');
expect(setting('enable_api'))->toBeTrue();
expect(setting('api_token'))->not->toBeNull();
get(route('api.user.index'))
->assertForbidden();
get(route('api.user.index'), ['Authorization' => 'Bearer wrong-secret'])
->assertForbidden();
get(route('api.user.index'), ['Authorization' => 'Bearer secret'])
->assertOk();
});