Store hashed tokens (bearer and api) instead of clear

This commit is contained in:
Jérémy Munsch 2021-08-09 17:35:28 +02:00
parent 53e76d86e6
commit 20935014c4
12 changed files with 102 additions and 81 deletions

View file

@ -29,10 +29,8 @@ class ApiController extends Controller
throw new ApiErrorException(ApiError::authFailed());
}
$service->generateApiToken();
$apiToken = $service->generateApiToken();
$service->saveOrFail();
return response()->json([
'api_token' => $service->api_token
]);
return response()->json(['api_token' => $apiToken]);
}
}

View file

@ -40,11 +40,11 @@ class ServiceController extends Controller
public function generateApiToken(Service $service)
{
$service->generateBearerToken();
$bearerPwd = $service->bearer_pwd;
$bearerToken = $service->generateBearerToken();
$bearerPwd = $service->bearer_pwd;
$service->saveOrFail();
return back()
// phpcs:ignore Generic.Files.LineLength.TooLong
->with('success', "Nouveau token d'API de $service->name : '$service->bearer_token' et mot de passe '$bearerPwd'");
// phpcs:ignore Generic.Files.LineLength.TooLong
->with('success', "Nouveau token d'API de $service->name : '$bearerToken' et mot de passe '$bearerPwd'");
}
}

View file

@ -28,40 +28,6 @@ class Authenticate extends Middleware
public function handle($request, Closure $next, ...$guards)
{
$isApiV1Request = $request->isJson() && Helper::isRequestForApiV1($request);
$isApiV2Request = $request->isJson() && Helper::isRequestForApiV2($request);
if ($isApiV2Request) {
// https://stackoverflow.com/questions/25838183/what-is-the-oauth-2-0-bearer-token-exactly/25843058
/**
* Un token devrais être fourni pour chaque requete apres authentification
* api_token param est requis par Auth
*/
$apiToken = $request->api_token;
if (is_null($apiToken)) {
$this->unauthenticated($request, $guards);
}
/**
** All ApiV2 routes must pass Brand model to controller action since a Brand owns
** a service token, otherwise the api cannot be called.
** This hack is a custom binder instead of passing
** this to route as a parameter
*/
if (!($route = Helper::getRoute($request))) {
$this->unauthenticated($request, $guards);
}
try {
$service = Service::getFromApiToken($apiToken);
$service->authenticateApi($guards, $apiToken);
app()->bind(Service::class, function () use ($service) {
return $service;
});
} catch (ModelNotFoundException $e) {
throw new AuthenticationException('Unauthenticated.', $guards);
}
}
// TODO: check this works
if ($isApiV1Request) {

View file

@ -8,6 +8,7 @@ use App\Models\ServiceStat;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class CheckBrand
{
@ -28,21 +29,22 @@ class CheckBrand
} else {
$this->apiToken = ($request->api_token !== null ? $request->api_token : null);
}
print_r($request->all());
if ($this->apiToken === null) {
die("Le token d'authentification est incorrect");
abort(401, 'Le token d\'authentification est incorrect');
}
$brand = Brand::whereHas('services', function (Builder $query) {
$query->where("api_token", hash('sha256', $this->apiToken));
$query->where("bearer_token", Hash::make($this->apiToken));
})->first();
if (!$brand) {
die("Le token d'authentification est incorrect");
abort(401, 'Le token d\'authentification est incorrect');
}
$serviceStat = ServiceStat::firstOrNew(['date' => date('Y-m-d')]);
$serviceStat->brand_id = $brand->id;
$serviceStat->service_id = Service::where('api_token', hash('sha256', $this->apiToken))->first()->id;
$serviceStat->service_id = Service::where('bearer_token', Hash::make($this->apiToken))->first()->id;
$serviceStat->nb_calls = $serviceStat->nb_calls === 0 ? 1 : $serviceStat->nb_calls + 1;
$serviceStat->save();

View file

@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use App\Lib\Helper;
use Closure;
use Illuminate\Http\Request;
@ -16,7 +17,8 @@ class JsonOnly
*/
public function handle(Request $request, Closure $next)
{
if (!$request->isJson()) {
// TODO reve v1 check when dropped
if (!Helper::isRequestForApiV1($request) and !$request->isJson()) {
abort(400);
}

View file

@ -2,7 +2,6 @@
namespace App\Models;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
@ -19,16 +18,28 @@ class Service extends Model
protected static function booted()
{
static::creating(function ($service) {
static::creating(function (self $service) {
$service->generateBearerToken();
if (Hash::needsRehash($service->bearer_token)) {
$service->bearer_token = Hash::make($service->bearer_token);
}
if (Hash::needsRehash($service->bearer_pwd)) {
$service->bearer_pwd = Hash::make($service->bearer_pwd);
}
if (Hash::needsRehash($service->api_token)) {
$service->api_token = Hash::make($service->api_token);
}
});
static::updating(function ($service) {
static::updating(function (self $service) {
if (Hash::needsRehash($service->bearer_token)) {
$service->bearer_token = Hash::make($service->bearer_token);
}
if (Hash::needsRehash($service->bearer_pwd)) {
$service->bearer_pwd = Hash::make($service->bearer_pwd);
}
if (Hash::needsRehash($service->api_token)) {
$service->api_token = Hash::make($service->api_token);
}
});
}
@ -75,12 +86,18 @@ class Service extends Model
* Get Service from apiToken with its brand
*
* @param string $apiToken
* @return \App\Models\Service|null
* @return \App\Models\Service
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public static function getFromApiToken(string $apiToken)
public static function getFromApiToken(string $apiToken): self
{
return static::where('api_token', $apiToken)->with('brand')->firstOrFail();
$services = static::all();
foreach ($services as $service) {
if (Hash::check($apiToken, $service->api_token)) {
return $service;
}
}
throw new ModelNotFoundException('Could not find service from Api Token');
}
/**
@ -90,9 +107,15 @@ class Service extends Model
* @return \App\Models\Service
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public static function getFromBearerToken(string $bearerToken)
public static function getFromBearerToken(string $bearerToken): self
{
return static::where('bearer_token', $bearerToken)->with('brand')->firstOrFail();
$services = static::all();
foreach ($services as $service) {
if (Hash::check($bearerToken, $service->bearer_token)) {
return $service;
}
}
throw new ModelNotFoundException('Could not find service from Service Token');
}
/**
@ -100,17 +123,12 @@ class Service extends Model
*
* @param array $guards
* @param string $apiToken
* @return void
* @throws \Illuminate\Auth\AuthenticationException
* @return bool
*/
public function authenticateApi(array $guards, string $apiToken)
public function authenticateApi(string $apiToken): bool
{
if ($this->api_token !== $apiToken) {
throw new AuthenticationException('Unauthenticated.', $guards);
}
if ((abs(strtotime($this->api_token_date) - time()) * 60) > (config('api.tokenTimeout') * 60)) {
throw new AuthenticationException('Unauthenticated.', $guards);
}
return Hash::check($apiToken, $this->api_token) and
((abs(strtotime($this->api_token_date) - time()) * 60) < (config('api.tokenTimeout') * 60));
}
/**

View file

@ -2,7 +2,12 @@
namespace App\Providers;
use App\Lib\Helper;
use App\Models\Service;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
@ -23,5 +28,39 @@ class AuthServiceProvider extends ServiceProvider
public function boot()
{
$this->registerPolicies();
Auth::viaRequest('api-token', function (Request $request) {
// https://stackoverflow.com/questions/25838183/what-is-the-oauth-2-0-bearer-token-exactly/25843058
/**
* Un token devrais être fourni pour chaque requete apres authentification
* api_token param est requis par Auth
*/
$apiToken = $request->api_token;
if (is_null($apiToken)) {
return null;
}
/**
** All ApiV2 routes must pass Brand model to controller action since a Brand owns
** a service token, otherwise the api cannot be called.
** This hack is a custom binder instead of passing
** this to route as a parameter
*/
if (!($route = Helper::getRoute($request))) {
return null;
}
try {
$service = Service::getFromApiToken($apiToken);
app()->bind(Service::class, function () use ($service) {
return $service;
});
$test = $service->authenticateApi($apiToken);
return $test ? $service : null;
} catch (ModelNotFoundException $e) {
return null;
}
});
}
}

@ -1 +1 @@
Subproject commit c9de85693e56511fc8774b051523dcd9911a908f
Subproject commit 5427646967e5c4e02558b564ba645263e2cb1d07

View file

@ -42,9 +42,7 @@ return [
],
'api' => [
'driver' => 'token',
'provider' => 'services',
'hash' => false, // Says that tokens are already hashed in DB
'driver' => 'api-token'
],
],
@ -69,12 +67,7 @@ return [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'services' => [
'driver' => 'eloquent',
'model' => App\Models\Service::class,
],
]
],
/*

View file

@ -83,10 +83,11 @@ class ApiBase extends BaseTestCase
$service = $brand->services()->firstOrFail();
// regen bearer token to get the pwd in clear
$service->generateBearerToken();
$bearer_pwd = $service->bearer_pwd;
$bearer_token = $service->generateBearerToken();
$bearer_pwd = $service->bearer_pwd;
$service->saveOrFail();
$service->bearer_pwd = $bearer_pwd;
$service->bearer_token = $bearer_token;
$service->bearer_pwd = $bearer_pwd;
// get customer info
$info = $customer->info($brand)->firstOrFail();

View file

@ -6,6 +6,7 @@ use App\Enums\ApiError;
use App\Enums\ApiStatus;
use App\Models\Service;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Hash;
use Tests\CreatesApplication;
class ApiTest extends ApiBase
@ -54,6 +55,6 @@ class ApiTest extends ApiBase
$this->assertApiResponseStatusIs($json, ApiStatus::OK());
$this->assertNotEmpty($json->json('data'));
$this->assertNotEmpty($json->json('data.api_token'));
$this->assertEquals(Service::findOrFail($service->id)->api_token, $json->json('data.api_token'));
$this->assertTrue(Hash::check($json->json('data.api_token'), Service::findOrFail($service->id)->api_token));
}
}

View file

@ -62,10 +62,11 @@ abstract class CrmClientTester extends TestCase
$info = $customer->info($brand)->firstOrFail();
// regen bearer token to get the pwd in clear
$service->generateBearerToken();
$bearer_pwd = $service->bearer_pwd;
$bearer_token = $service->generateBearerToken();
$bearer_pwd = $service->bearer_pwd;
$service->saveOrFail();
$service->bearer_pwd = $bearer_pwd;
$service->bearer_token = $bearer_token;
$service->bearer_pwd = $bearer_pwd;
return compact('customer', 'service', 'brand', 'login', 'info');
}