I have a Laravel version 6 application. I use JWT package for authentication.
The routes defined like this:
// route/api.php
Route::middleware('auth:api')->group(function () {
Route::apiResources([
'genders' => 'API\GenderController'
]);
});
Inside the controllers I have 5 functions index, store, show, update and destroy.
I need to set the locale for the current user before running any code inside these functions, something like this:
public function index()
{
$locale = request()->user()->lang;
app()->setLocale($locale);
// Rest of the function
}
My controller class extends a BaseController class. As the Restful API is state less I need to set the locale each time the user send a API request. Now the question is that where is the best place to do it and how can I do it?
I tried to do it inside the constructor of the BaseController but it seems the middleware('auth:api') has not yet checked the token in constructor.
The other option is to set the locale inside a authentication event handler. I found a list of JWT package events here:
// fired when the token could not be found in the request
Event::listen('tymon.jwt.absent');
// fired when the token has expired
Event::listen('tymon.jwt.expired');
// fired when the token is found to be invalid
Event::listen('tymon.jwt.invalid');
// fired if the user could not be found (shouldn't really happen)
Event::listen('tymon.jwt.user_not_found');
// fired when the token is valid (User is passed along with event)
Event::listen('tymon.jwt.valid');
If someone could help me to define a handler for tymon.jwt.valid I would be appreciated. Or even if you have some other solution for running an event before execution of index, store, show, update and destroy functions.
You can easily do it in the middleware. Just put middleware that authenticates user before the one that sets location.
Create your route class and register it in App\Http\Kernel::$routeMiddleware
Then use it like this:
// rouite/api.php
Route::group(['middleware' => ['auth:api', 'locale']], function () {
Route::apiResources([
'genders' => 'API\GenderController'
]);
});
I could find a different way for accessing the current user inside the constructor class. While the request()->user() code returns null in constructor, auth()->user() return the current user even in the constructor.
abstract class BaseController extends Controller
{
public function __construct()
{
$locale = auth()->user()->lang;
app()->setLocale($locale);
}
}
Therefore I could set the locale in my BaseController class.
However, my question regarding working with JWT events is open. Also I would like to know more details about working with middleware as #patryk-karczmarczyk suggested in his answer.
Related
Using Laravel 7.
In the controller constructor, I then hoped to get access to the current user details so I could load main site widgets (buttons links etc) and custom user widgets in to one to be displayed in the view
use Illuminate\Support\Facades\Auth;
...
$widgets = Cache::get("widgets");
$usersdata = Cache::get("userdata");
$this->middleware('auth');
$widgets = array_merge($widgets, $usersdata[Auth::user()->id]["widgets"]);
View::share([
"widgets" => json_encode($widgets)
]);
however at this stage from research the user data is not available (even after authentication ?).
Not sure of best way to access this, or better practice might be to override the middleware auth (where?) so that it could return user id or something eg:
$userid=$this->middleware('auth');
I would like this in the constructor so the same method is in place for all controllers which extend this main controller.
This is intended behavior from laravel, you can read more about it here.
Laravel collects all route specific middlewares first before running
the request through the pipeline, and while collecting the controller
middleware an instance of the controller is created, thus the
constructor is called, however at this point the request isn’t ready
yet.
You can find Taylor's reasoning behind it here:
It’s very bad to use session or auth in your constructor as no request
has happened yet and session and auth are INHERENTLY tied to an HTTP
request. You should receive this request in an actual controller
method which you can call multiple times with multiple different
requests. By forcing your controller to resolve session or auth
information in the constructor you are now forcing your entire
controller to ignore the actual incoming request which can cause
significant problems when testing, etc.
So one solution would be to create a new middleware and then apply it to all routes, something like this, where widgets is your new middleware:
Route::group(['middleware' => ['auth', 'widgets']], function () {
// your routes
});
But if you really want to keep it in the constructor you could implement the following workaround:
class YourController extends Controller
{
public function __construct(Request $request)
{
$this->middleware('auth');
$this->middleware(function ($request, $next) {
$widgets = Cache::get("widgets");
$usersdata = Cache::get("userdata");
$widgets = array_merge($widgets, $usersdata[$request->user()->id]["widgets"]);
View::share([
"widgets" => json_encode($widgets)
]);
return $next($request);
});
}
}
tl;dr; How can I get session data, then based on authentication, manipulate routes in a laravel ServiceProvider?
The Problem
I need to do two things in a service provider. Register routes and use session data at the same time potentially.
Existing Solutions
Route Without Session Data
I know ServiceProvider::register may happen before RouteProvider::register gets called. ServiceProvider::boot can add routes using something like this...
public function boot(Router $router)
{
$router->prefix('api')->group(function ($router) {
$router->resource('lists', '\App\Extensions\ExtensionName\Http\Controllers\MyController');
});
}
Now this will add the route properly. But by the time that route is accessed, there will be no session data. Therefore Auth::user() will return null. And of course Auth::user() is going to return null in the boot() method as well during the adding of routes.
Session Data Without Route
I could extend the \Illuminate\Session\Middleware\StartSession with my own middleware and fire an event like session.started at the end of an overloaded startSession() method. With this, I should have the session data needed to do something like Auth::user().
public function boot(Router $router)
{
Event::listen('session.started', function () use ($router) {
if (Auth::user()->can('do.something')) {
$router->middleware(['auth'])->prefix('api')->group(function ($router) {
$router->resource('lists', '\App\Extensions\ExtensionName\Http\Controllers\MyController');
});
}
});
}
However, the route will not be added at this point as that stage has already been long over with.
Proper Solution Hints
I've been leaning towards registering my routes, then in my controller, I would somehow inject the session middleware so that it starts the session before my controller code actually runs. But I'm unsure how to do something like that.
It would be very nice if I could have access to the session data before even supplying my routes as it would be cleaner, faster and more secure to include routes that users have access to in the first place instead of checking on every call to them or removing them after they've been added and authorization has been checked.
References
How to retrieve session data in service providers in laravel?
https://laracasts.com/discuss/channels/general-discussion/laravel-5-session-data-is-not-accessible-in-the-app-boot-process
https://github.com/laravel/framework/issues/7906
https://github.com/laravel/framework/pull/7933
https://laravel.io/forum/12-17-2014-session-content-not-available-in-service-providers-register
https://josephsilber.com/posts/2017/01/23/getting-current-user-in-laravel-controller-constructor
I got the following serivce provider:
class CartServiceProvider extends ServiceProvider {
public function boot() {
}
public function register() {
$this->app->singleton(\Alexxosipov\Cart\Cart::class, function($app) {
return new \Alexxosipov\Cart\Cart($app['request']);
});
}
}
And my Cart class, where I got this:
$this->id = (request()->cookie('cart_id')) ? request()->cookie('cart_id') : false;
But request()->cookie('cart_id') returns encrypted string. If I will do it in any controller, it works fine. Why? What should I do to use it in Cart class?Laravel 5.5
The sequences of Laravel bootstrapping a request is this (v5.6): (CMIIW)
index.php is called
\Illuminate\Foundation\Application created
HttpKernel registered. This register your middlewares
Console Kernel registered. This defines console commands
Exception Handler registered. This defines exception handlers
HttpKernel instantiated. This instantiate all middlewares, plus boot/register all your service providers
Create global request instance. Pass it to HttpKernel to handle incoming request.
Middleware EncryptCookies called, cookies decrypted
Sending request to other middlewares to process
Send request to router, router dispatch to controller
...
Before sending response to browser, cookies encrypted back in EncryptCookies
Cookie is remained encrypted in Step 1 - Step 7. Your CartServiceProvider is trying to obtain a cookie that is yet to be decrypted at Step 6, which is not possible. Consider either
Decrypt the cookie by yourself (using just decrypt), or
Make a middleware to instantiate the cart after EncryptCookies. It's a little bit too early to instantiate cart at the bootstrapping service providers phase.
Edit: Add singleton suggestion
I think you could do this:
Create a new method named loadCartFromRequest($request) in your Cart::class. This method help you load a cart instance from request during the middleware phase.
In your CartServiceProvider, you register a singleton of Cart::class as usual, but no need to read the request here.
Create a middleware, called CartMiddleware. This middleware call app(Cart::class)->loadCartFromRequest($request).
Then at any other places that you need the cart instance, you can access your cart model from app(Cart::class).
I hope I understand your requirement correctly :)
Why? Cookie encryption protects data stored in the client's browser.
The How: Laravel uses the EncryptCookies middleware, this middleware would not yet be processed when your service provider is registered but would be processed for the controller since middlewares are in the routing stack before the request is passed to the controller.
Since I don't know about your cart class and its logic, I can't really recommend what you should do. Perhaps you need to think about when you are passing the Request object to your class.
Problem
It seems like my API-handling controller cannot process an injected Request object. Details below.
I have a route in my api.php routes file:
Route::namespace('Mailing')->prefix('mailing/webhook')->group(function () {
Route::post('open', 'WebhookController#getOpened');
});
In the WebhookController.php, there's a constructor with the injected Request object:
use Illuminate\Http\Request;
public function __construct(Request $request) {
// Log it in the database
Logger::info('constructor called...');
// Filter the input in the injected Request
$this->input = $this->getFilteredInput($request);
}
Now, when I deploy this code to the server and make the API call against my endpoint, the log message at the beginning of the __construct() method is not even fired. I also get no data stored in the database (which should happen if the webhook is correctly processed).
What I tested
However, when I create a dummy route in web.php routes file:
Route::post('open', 'WebhookController#getOpened');
and then create a form and make a POST call to the getOpened() method - all works as expected: I have the Request injected and can manipulate it.
I checked that:
The API endpoint is correct and available
There are no obsolete use statements at the top of the
WebhookController
Questions
Why does the api.php route fail on me? Doesn't Laravel's dependency injection work for API-handling controllers?
I am today in bit confusion about my website security and some extra code that is written to make the website secure. Below are 2 locations where security is applied.
Inside Route Config, To secure the route, I have used Middleware to check the user role.
Route::group(['middleware' => ['web', 'SuperAdmin', 'auth']], function () {
Route::get('/Create-Department', 'DepartmentController#CreateDepartment');
});
I mentioned 2 Middlewares.
Auth Middleware : This is for authentication.
SuperAdmin Middleware: This is for Authorization.
Second location is Request class. Here is the code. In authorize method, again same thing is being checked as already done in route
class DepartmentRequest extends Request
{
public function authorize()
{
if(\Auth::user() == null) {
return false;
}
if(\Auth::user()->isSuperAdmin()) {
return true;
}
return false;
}
public function rules()
{
return [
'Department' => 'required',
];
}
}
Question: Should I remove check in Request class? Is that an unwanted validation to secure the request ? As route.config is already doing the job.
What's the use of authorize method? I meant, I am using Request class to validate form inputs. Should I always return true from authorize method?
yes, you should remove that checks in the Request class: if you're already doing that checks in your middleware you should not repeat them
When you specify this:
Route::group(['middleware' => ['web', 'SuperAdmin']], function () {
Route::get('/Create-Department', 'DepartmentController#CreateDepartment');
});
You're telling laravel that, when it finds a /Create-Department route, it should trigger the handle method of these middleware: ['web', 'SuperAdmin'], before the request is sent to the DepartmentController
So, if you check for authentication and authorization in the middlewares, when the request will get to your controller you're sure that it has satisfied all the middleware it went through
Regarding the purpose of the authorize method: the authorize method is usually used to authorize the actual request basing on some policy you'd like to respect. For example, if you have a request to edit a Post model, in the authorize method you'd check that the specific user trying to edit the post has the permissions to do it (for example being the author of the post )
EDIT
Even if you want to use a middleware for your authorization, it's fine. Anyhow, usually the authorize method within form requests is used to do authorization checks on the specific request.
For instance check this example from the docs :
public function authorize()
{
$postId = $this->route('post');
//here the authorization to edit the post is checked through the Gate facade
return Gate::allows('update', Post::findOrFail($postId));
}
In conclusion: if you're doing your authentication and authorization tasks in middlewares, you don't need to repeat them in the authorize method, but keep in mind that the native purpose of the method is to authorize the specific request