I'm currently building a SPA type of application prototype. The first step is to implement an API with Laravel Passport and secure it. For that, I'm taking inspiration of this existing structure : Laravel SPA. The problem is that, none of the API URL's are protected which means, as a user, I can request all informations from the API.
So I decided to start from scratch with more security. I'm using a role and permission package which is : Laravel-permission.
This is the first time I'm implementing and API and I was stuck on the concept of scopes with Laravel passport, because they can be add directly in the API request, without a checking based on the role of the user.
I found someone who gave a solution on StackOverflow which can be found here : Role based API url protection.
So here is my implementation :
// AuthServiceProvider
public function boot()
{
$this->registerPolicies();
// We define all the scopes for the tokens
Passport::tokensCan([
'manage-continents' => 'Manage continents scope',
'read-only-continents' => 'Read only continents scope',
]);
Passport::routes();
}
Then, I'm creating a REST Controller with Laravel ressources controller.
// Rest Controller
namespace App\Http\Controllers\API\GeoLocation;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\GeoLocation\Continent as Continent;
class ContinentController extends Controller
{
public function index()
{
// allow listing all continents only for token with manage continent scope
return Continent::all();
}
public function store(Request $request)
{
// allow storing a newly created continent in storage for token with manage continent scope
}
public function show($id)
{
// allow displaying the continent for token with both manage and read only scope
}
}
Then in the api.php file, I'm adding the following routes :
Route::get('/continents', 'API\GeoLocation\ContinentController@index')
->middleware(['auth:api', 'scopes:manage-continents']);
Route::post('/continents', 'API\GeoLocation\ContinentController@store')
->middleware(['auth:api', 'scopes:manage-continents']);
Route::get('/continents/{id}', 'API\GeoLocation\ContinentController@show')
->middleware(['auth:api', 'scopes:manage-continents, read-only-continents']);
I'm then creating a Controller to intercept the request, and to add scopes based on the user role. Problem, I think this method is never reached as I'll explain after.
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
class ApiLoginController extends Controller
{
use AuthenticatesUsers;
protected function authenticated(Request $request, $user)
{
// Implement your user role retrieval logic
// for example retrieve from `roles` database table
$roles = $user->getRoleNames();
$request->request->add(['username' => $request->email]);
// @TODO to avoid many requests, we should just deepdive into
// the collection returned by the role
// grant scopes based on the role that we get previously
if ($roles->contains('hyvefive_super_administrator'))
{
// grant manage order scope for user with admin role
$request->request->add([
'scope' => 'manage-continents'
]);
}
else
{
// read-only order scope for other user role
$request->request->add([
'scope' => 'read-only-continents'
]);
}
// forward the request to the oauth token request endpoint
$tokenRequest = Request::create(
'/oauth/token',
'post'
);
return Route::dispatch($tokenRequest);
}
}
The last step before testing everything is to add the route of that controller in the api.php file :
Route::post('login', 'Auth\ApiLoginController@login');
Finally, to test, I'm just using the HomeController from the Auth default Laravel package as following :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use GuzzleHttp\Client;
use Auth;
class HomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
/**
* Show the application dashboard.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
// from client application
$http = new Client();
$response = $http->post('http://hyvefivity.local/api/login', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 4,
'client_secret' => 'fsW4E5fcQC0TGeVHOrvr1qlZ8TEgrgpSRziVLCDS',
'username' => 'myemail@gmail.com',
'password' => 'my-secret-password',
'scope' => 'manage-continents',
],
]);
// You'd typically save this payload in the session
$auth = json_decode((string) $response->getBody(), true);
var_dump($auth);
/*$response = $http->get('http://hyvefivity.local/api/continents', [
'headers' => [
'Authorization' => 'Bearer '.$auth->access_token,
]
]);
$continents = json_decode( (string) $response->getBody() );
*/
// return view('home');
}
}
The problem is that I feel the ApiLoginController is never reached, and same goes for the authenticated method.
If I'm doing the following :
$http = new Client();
$response = $http->post('http://hyvefivity.local/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 4,
'client_secret' => 'fsW4E5fcQC0TGeVHOrvr1qlZ8TEgrgpSRziVLCDS',
'username' => 'my-email@gmail.com',
'password' => 'my-seret-password',
'scope' => 'manage-continents',
],
]);
A token is generated, but not with the scopes I've added in the ApiLoginController.
Another improvement I would like to do in addition is : should I do that API call during the login because, if I'm doing it in the HomeController, the problem is that the password is hashed, which means it's impossible to ask for a Password Grant type of token without having the password during the login ?