I am new to Laravel and was wondering what would; be the best practice to prevent users editing each other data.
I am aware that I can handle users pages with filter and Auth, eg.
Route::filter('auth', function($route)
{
$id = $route->getParameter('id');
if( Auth::check() && Auth::user()->id != $id) {
return Redirect::route('forbidden');
}
});
However I was wondering what about relationship pages (i.e. /user_profile/14, user_settings/22 , etc ). Do I have define filter for each of these [group of] routes and check id's against the relationship?? e.g.
Route::filter('auth.user_settings', function($route)
{
$id = $route->getParameter('id');
if( Auth::check() && Auth::user()->user_settings->id != $id) {
return Redirect::route('forbidden');
});
Route::filter('auth.user_profile', function($route)
{
$id = $route->getParameter('id');
if( Auth::check() && Auth::user()->profile->id != $id) {
return Redirect::route('forbidden');
});
...etc
or is there a better way to do this??
I'm guessing you did your application in a way that your route might look like this:
Route::get('user_profile/{id}', function()
If that's how you've been defining the routes for your user you might want to change it to just:
Route::get('user_profile', function()
Then upon requesting, the controllers would then load the appropriate profile/setting from the current authenticated user using something like Profile::findOrFail(Auth::user()->id) which in this case you don't have to worry about user checking out others profile, and all you need is the 'auth' filter group for the routes.
Related
I have implemented the simplest example using the Spatie docs for multitenancy, that is working perfectly fine. Now, I intend to use multiple second-level domains for each tenant I have.
For example; I have 2 tenants company-a and company-b and they are being served at company-a.localhost and company-b.localhost, now what I want is that when I visit company-a.admin.localhost, it should tell me COMPANY-A ADMIN and If I visit company-a.employee.localhost, it should tell me COMPANY-A EMPLOYEE.
I have tried using subdomain on routes in RouteServiceProvider like the following:
Route::middleware('web')
->group(base_path('routes/security.php'));
Route::domain($this->baseDomain('admin'))
->middleware('web')
->name('admin.')
->group(base_path('routes/admin.php'));
Route::domain($this->baseDomain('employee'))
->middleware('web')
->name('employee.')
->group(base_path('routes/employee.php'));
private function baseDomain(string $subdomain = ''): string
{
if (strlen($subdomain) > 0) {
$subdomain = "{$subdomain}.";
}
return $subdomain . config('app.base_domain');
}
Without subdomain, it works fine, but the routes with second-level domain, it falls to base level domain route and does not get the current tenant.
What am I missing here? Is this even possible to implement.
Thankyou.
Take, for example, the route:
Route::domain('{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
});
The binding fields would be ['subdomain', 'param1', 'param2'], and the compiled route would have it's regexes declared as
regex => "{^/foo/(?P<param1>[^/]++)/(?P<param2>[^/]++)$}sDu",
hostRegex => "{^(?P<subdomain>[^\.]++)\.example\.com$}sDiu"
Where ^(?P<subdomain>[^\.]++)\. will explicitly stop capturing when finding a dot, in this case the delimiter between groups.
However, these regexes are overridable by using the where method. You could declare the above route as
Route::domain('{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
})->where('subdomain', '(.*)');
In the compiled route , the hostRegex would be now
hostRegex => "{^(?P<subdomain>(?:.*))\.example\.com$}sDiu"
Meaning it will capture anything preceding .example.com. If you requested company-a.admin.example.com, $subdomain would be company-a.admin.
You could also declare a route with two binding fields in its domain:
Route::domain('{subsubdomain}.{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
});
Which might be more useful if you wanted subsubdomains to imply a hierarchy.
I have achieved this by using some checks, in RouteServiceProvider, I have not used the actual domain function on Route like we do normally i.e. Route::domain('foo.bar'). The reason was that, the Spatie package use a kind of middleware Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class which runs whenever we hit the domain with tenant comapny-a.localhost. And it gets the tenant from hostname i.e comapny-a.localhost.
public function findForRequest(Request $request):?Tenant
{
$host = $request->getHost();
return $this->getTenantModel()::whereDomain($host)->first();
}
In my RouteServiceProvide:
$this->routes(function () {
$class = 'security';
$middleware = 'web';
if (Str::contains(request()->getHost(), 'admin')) {
$class = 'admin';
} elseif (Str::contains(request()->getHost(), 'employee')) {
$class = 'employee';
} elseif (Str::contains(request()->getHost(), 'api')) {
$class = 'api';
$middleware = 'api';
}
Route::middleware($middleware)
->name("$class.")
->group(base_path("routes/${class}.php"));
});
As In my scenario, I had only these 2 kind of second-level domains and so, I just checked if this particular keyword exists in the hostname and choosing the file and middleware accordingly.
I also overrided the DomainTenantFinder class and in multitenancy.php config file:
public function findForRequest(Request $request): ?Tenant
{
$host = $request->getHost();
$host = str_replace('admin.', '', $host);
$host = str_replace('employee.', '', $host);
$host = str_replace('api.', '', $host);
$tenant = $this->getTenantModel()::whereDomain($host)->first();
if (empty($tenant)) {
abort(404);
}
return $tenant;
}
I have acheived the desired outcome, however, I have a security concern, specially in RouteServiceProvider logic. Thought??
i've a function in my admin model which has connected to role model by many to many relationship. i created a middleware to check the roles of admins and redirect them to their dashboard, but i'm not sure it is returning the correct value. here is the way i'm checking -> Auth::check() && Auth::user()->role(). i mean the role() method is returning the role_id but how to check it ? like Auth::check() && Auth::user()->role()->role_id == 1, though this is not a correct way
You can create a function in your User(or Admin whichever is authenticatable) Model :
<?php
public function hasRoles(array $roles)
{
return $this->roles()->whereIn('role_id', [$roles])->exists();
}
// Then you can check using ids
const ADMIN_ROLE = 1;
const OTHER_ROLE = 2;
if(Auth::user() && Auth::user()->hasRoles([self::ADMIN_ROLE, self::OTHER_ROLE])){
// Current user is either admin or other role
}
If you have name of a role then you can use that instead of ids as well.
Here is the better way.
In user model, write a function something like this .
public function hasRole( ... $roles ) {
foreach ($roles as $role) {
if ($this->roles->contains('column_name', $role)) {
return true;
}
}
return false;
}
in middleware, you'd write something like this
if(Auth::user() && Auth::user()->hasRole("admin", "moderator)){
return $next($request);
}
This way you'd not check for ids , because ids might change.
In laravel 5.2, i want to add the condition so that only users where their expiry date is greater than today's date to login.
protected function getCredentials(Request $request)
{
return ['email' => $request->{$this->loginUsername()}, 'password' => $request->password];
}
The code does not accept adding:
'expires' => gte(Carbon::now())
Any help is appreciated
I don't think this is possible, even in Laravel 5.5. Taking a look at the retrieveByCredentials method in Illuminate\Auth\EloquentUserProvider which is used to get the user from the database, you can see that the query passes simple key/value combinations to the where method on the $query object, which equate to where key = value. This is from 5.5:
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('password', $credentials))) {
return;
}
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
if (! Str::contains($key, 'password')) {
$query->where($key, $value);
}
}
return $query->first();
}
To achieve what you are after I would recommend doing this check after the user has logged in, in your controller for instance:
// Imagine this is the controller method where you're dealing with user logins
public function login(array $credentials)
{
if (! auth()->attempt($credentials)) {
// Handle what happens if the users credentials are incorrect.
}
$user = auth()->user();
if (Carbon::now()->gte($user->expires)) {
// User's account has expired, lets log them out.
auth()->logout();
// Return a redirect with a message or something...
}
// Handle a successful login.
}
I'm not sure if the auth() helper is available in 5.2, but you should be able to use the Auth facade to do the same thing, e.g. Auth::attempt(...).
I am very new to realtime event broadcasting, I have simple laravel-echo-server setup and working with everything. I am unable to set/define authentication against other auth guard it is always checking with user/default guard defined in auth.php
I have setup the authentication routes for each guards private channels in routes/channel.php as below per documentation.
For auth guard user private channels
Broadcast::channel('users.{id}', function ($user, $id) {
Log::info(class_basename($user));
return (int) $user->id === (int) $id;
});
For auth guard admin private channels
Broadcast::channel('admins.{id}', function ($admin, $id) {
Log::info(class_basename($admin));
return (int) $admin->id === (int) $id;
});
It works fine for guard user that is the first case but never worked for the second one i.e. admin guard.
and the
Log::info(class_basename($admin)) always returns User class.
So, how do we pass or define that it should use admin guard instead of user.
after exploring the inside of Illuminate\Broadcasting\Broadcasters\Broadcaster I found out that below in line 411
public function user($guard = null)
{
return call_user_func($this->getUserResolver(), $guard);
}
So, if we can pass this guard parameter it can solve the purpose.
If anyone can give me anything or way of authorising with multiple guard setup that will be very helpfull.
Using Laravel 5.4, laravel-echo-server, Redis, Socket.IO
I finally got this to work with 2 separate login screens and 2 separate users and customers tables.
Firstly I followed the Laracasts video on private channel broadcasting. The video says to put all the echo event listeners in your bootstrap.js. This will work with one users table. However for 2 separate tables users, customers you need to place the relevant echo event listeners in your 2 separate app.blade.php layouts files. For users in one and for customers in the other. However the listeners should be positioned at the bottom.
window.Echo.private('App.User.' + window.Laravel.user.id) .listen('Event', e => { etc. });
window.Laravel = {!! json_encode([ 'customer' => auth()->guard('customer')->user() ]) !!};
window.Echo.private('App.Customer.' + window.Laravel.customer.id) .listen('Event', e => { etc. });
Then in your routes/channels.php
Broadcast::channel('App.User.{id}', function ($user, $id)
return (int) $user->id === (int) $id;
});
Broadcast::channel('App.Customer.{id}', function ($user, $id)
return (int) auth()->guard('customer')->user()->id === (int) $id;
}); // Note I do not compare "$user" here
Then in BroadcastServiceProvider.php
Broadcast::routes(['middleware' => 'web', 'auth:customer']);
require base_path('routes/channels.php');
//Remove Broadcast::routes();
A customer can receive a private message and so can a user. Hope this helps.
Just like in the otherwhere, simply use Request facade in the closure.
In your case:
Broadcast::channel('admins.{id}', function ($user, int $id) {
return Request::user('admin')->id === $id;
});
The arguments sent to the closure can not be change by user, it is controlled by laravel framework.
(see BroadcastManage and RedisBroadcaster, or other implementations of Illumiante\Contracts\Broadcasting\Broadcaster)
Im curious to know if it is possible to prevent users who don't have a role of owner or administrator from accessing certain controllers in a laravel application?
Yes you can. You can do this with a route filter.
routes.php
Route::group(['prefix' => 'admin', 'before' => 'auth.admin'), function()
{
// Your routes
}
]);
and in filters.php
Route::filter('auth.admin', function()
{
// logic to set $isAdmin to true or false
if(!$isAdmin)
{
return Redirect::to('login')->with('flash_message', 'Please Login with your admin credentials');
}
});
Route filters have already been proposed but since your filter should be Controller specific you might want to try controller filters.
First off, lets add this your controller(s)
public function __construct()
{
$this->beforeFilter(function()
{
// check permissions
});
}
This function gets called before a controller action is executed.
In there it depends on you what you want to do. I'm just guessing now, because I don't know your exact architecture but I suppose you want to do something like this:
$user = Auth::user();
$role = $user->role->identifier;
if($role !== 'admin' && $role !== 'other-role-that-has-access'){
App::abort(401); // Throw an unauthorized error
}
Instead of throwing an error you could also make a redirect, render a view or do basically whatever you want. Just do something that stops further execution so your controller action doesn't get called.
Edit
Instead of using Closure function, you can use predefined filters (from the routes.php or filters.php)
$this->beforeFilter('filter-name', array('only' => array('fooAction', 'barAction')));
For more information, check out the documentation