Laravel doesn't change user model on the go - php

I am using Laravel 4 for my app.
In this app I've got two auth models: Buyers and Users. I don't wont to use User->type field, because this models have absolutely different logic.
Here's my login Controller:
public function postIndex()
{
if (Auth::attempt(array_only(Input::get(), array('email', 'password')), array_only(Input::get(), array('save')))) {
Login::create(array('user_id' => Auth::user()->id, 'session_id' => session_id())); // Создаем новую запись логина вместе с session_id.
return Redirect::to('/');
}
return $this->userauth();
}
public function userauth() {
Config::set('auth.model', 'User');
Config::set('auth.table', 'users');
$test = Config::get('auth.model');
if (Auth::attempt(array_only(Input::get(), array('email', 'password')), array_only(Input::get(), array('save')))) {
Login::create(array('user_id' => Auth::user()->id, 'session_id' => session_id())); // Создаем новую запись логина вместе с session_id.
return Redirect::to('/');
}
Session::flash('error', 'Auth not excepted. '. implode(' ', array_only(Input::get(), array('email', 'password'))));
return Redirect::to('logins')->withInput(Input::except('password'));
}
I've already changed settings in auth.php to work with buyers. When I'm typing login and password for buyer, everything works great. It seems, that after Auth::attempted() it doesn't change settings. It looks like I have to reload Auth object. Can somebody help me?
Buy the way, if I write like this:
public function postIndex()
{
Config::set('auth.model', 'User');
Config::set('auth.table', 'users');
$test = Config::get('auth.model');
if (Auth::attempt(array_only(Input::get(), array('email', 'password')), array_only(Input::get(), array('save')))) {
Login::create(array('user_id' => Auth::user()->id, 'session_id' => session_id())); // Создаем новую запись логина вместе с session_id.
return Redirect::to('/');
}
Session::flash('error', 'Auth not excepted. '. implode(' ', array_only(Input::get(), array('email', 'password'))));
return Redirect::to('logins')->withInput(Input::except('password'));
}
everything works great too.

Short Answer:
You're right. After the first call to Auth::attempt() changes to the config have no effect. During runtime, you have to use Auth::setProvider() to set the model which will be used.
Long Answer:
I don't know how well this will work in your particular setup, but I found your question while trying to do something similar, so I'll show you how I ended up doing it.
In my case, the requirement wasn't simply to have two different types of users. I'm running two websites on separate domains from the same codebase, one for students and one for host families. I have a Student class that will replace User on the one site and Host for the same purpose on the other. Both implement UserInterface and RemindableInterface, so we're good there.
In app/filters.php, I create two special filters:
Route::filter('prep.student', function() {
Auth::setProvider(new Illuminate\Auth\EloquentUserProvider(App::make('hash'), 'Student'));
});
Route::filter('prep.host', function() {
Auth::setProvider(new Illuminate\Auth\EloquentUserProvider(App::make('hash'), 'Host'));
});
Here "Host" and "Student" are the class names you want the Eloquent authentication driver to use.
In app/routes.php, I put my routes into two groups, and on each group I used the proper filter above:
/**
* Routes for Students
*/
Route::group(array('domain' => '[student domain]', 'before' => 'prep.student'), function() {
Route::get('test', function() {
return View::make('student/test');
});
...
});
/**
* Routes for Hosts
*/
Route::group(array('domain' => '[host domain'], 'before' => 'prep.host'), function() {
Route::get('test', function() {
return View::make('host/test');
});
...
});
The result is that the user provider is set with the proper model for each group of routes. You could probably also set the provider in app/start/global.php, but I decided to keep it with routing so that it's very clear that the two groups of routes are for two different domains and have two different authentication models.
I would do this a bit differently were I building it new, but the two websites are the frontend of a large legacy codebase for which I can't significantly alter the database, so I freely admit this is a bit of a hack. If you're not in such a situation, a better solution might be to have a User class for authentication, and then attach that to the two other models you need.

Related

Laravel - Log In with only one device

Is there any way that I can allow user to login only from one device?
Thanks in advance
Well, you would need to check at a central place, if there is an already existing session for the user that currently want to log in - and if yes, delete all existing sessions.
The central place would proably be when the login happens or inside an auth middleware.
To delete all existing sessions for the user you can run
DB::table('sessions')->where('user_id', $user->id)->delete();
Log in only from one device, f. ex. Laptop
That is probably not possible as each device would need to send a unique identifier - which it doesn't. As example, your Laptop would need to send a unique identifier to the Laravel system, so that your Laravel application would know, that it is the Laptop the login is coming from.
The login forms normally only takes a username/email and a password, so no unique property to identify your Laptop.
You could probably check for browser user agent or things like this, but that is all fakeable and does not guarantee a 100% proof identification of the device.
You can use deviceInspect middleware and check user agent (it could be fake as #codedge said) and use it after auth middleware
As you can see the user will be authenticated but routes will be protected by device
Create middleware
class DeviceInspect
{
public function handle($request, Closure $next)
{
$user = Auth::user(); //or $request->user()
// TODO get enabled device/s from datebase for $user - by userId
$enabledDevice = "Dalvik/2.2.0 (Linux; U; Android 10.0.1; AM-A89R Build/NMB55D)"; //example
$currentDevice = $request->userAgent(); //or $_SERVER['HTTP_USER_AGENT'];
//it could be fake like codedge said
if ($enabledDevice !== $currentDevice) {
$data = array(
"device" => false,
"message" => "your message to user",
);
return response([$data], 401); // or something else
}
return $next($request);
}
}
add this to App\Http\Kernel
protected $routeMiddleware = [
...
'device' => 'App\Http\Middleware\DeviceInspect',
];
and use it like below
//in controller
class SomeController extends Controller {
public function __construct() {
parent::__construct();
$this->middleware(['auth', "device"]);
}
}
or
//Or in routes
Route::get('/profil', function () {
//
})->middleware(['auth', 'device']);
or
Route::group(['prefix' => '/v1/data', 'namespace' => 'Api\V1', 'as' => 'api.', 'middleware' => ['auth:api', 'device']], function () {
Route::resource('activity', 'Data\DataController', ['only' => ['index', 'show']]);
});

Laravel 6 - setting up a multi-tenant application with nested tenants

I am currently trying to figure out the best way to set up multi-tenancy for my system. The issue I'm facing is that a tenant doesn't always have to be a sub-domain but can be set up as part of a sub-domain, where the sub-domain can have multiple tenants. I can't seem to find anything online that would help me set this up in Laravel 6.
System Requirements:
A server can have many sub-domains
A sub-domain can be a tenant
A sub-domain can have many tenants
A tenant can have many users
a tenant can have different features
The system has to be set up with a single database that will use tenant_id to determine which data belongs to a tenant.
I am currently storing all sub-domain data in a table "subdomains" with the following structure:
id
subdomain (unique)
status
nested_tenants (yes/no)
where the column nested_tenants determines whether or not the sub-domain is a tenant(=0) itself or has multiple tenants(=1). if the sub-domain does not have nested tenants then we set tenant_id=subdomain
if the sub-domain does have nested tenants then we store all of these in a table with structure:
id
subdomain (the sub-domain it belongs to)
tenant (the tenant - unique field)
name
status
and we set tenant_id=tenant from this table.
if we have nested tenants for a sub-domain then we cannot determine what the current tenant is until the user logs in. we would have to get the tenant_id from the user details.
My current set up:
I've been following this article and have set up the following:
I have two models
Subdomain,
Tenant
Routes/web.php:
Route::group([
'middleware' => \App\Http\Middleware\IdentifySubdomain::class,
'as' => 'tenant:',
'namespace' => 'Tenant'
], function () {
// custom auth routes
Route::get('/login', 'Auth\LoginController#index')->name('login');
Route::post('/login', 'Auth\LoginController#login');
Route::get('/home', 'HomeController#index')->name('home');
});
Middleware IdentifySubdomain:
class IdentifySubdomain
{
protected $tenantManager;
public function __construct(TenantManager $tenantManager) {
$this->tenantManager = $tenantManager;
}
public function handle($request, Closure $next)
{
/** need to check whether subdomain is valid
* if subdomain is valid return the request page else error message.
* if subdomain is true it will check the nested_tenants value from db.
* if nested_tenants is false it will set the tenant to current subdomain
* else the tenant is not set yet.
*/
// get host domain and subdomain domain
$host = $request->getHost();
// get subdomain position
$pos = strpos($host, env('TENANT_DOMAIN'));
$subdomain = substr($host, 0, $pos - 1);
if ($pos !== false && $this->tenantManager->checkSubdomain($subdomain)) {
return $next($request);
}
throw new NotFoundHttpException;
}
}
TenantManager:
class TenantManager {
private $tenant;
public function setTenant(?Tenant $tenant) {
$this->tenant = $tenant;
return $this;
}
public function getTenant(): ?Tenant {
return $this->tenant;
}
public function loadTenant(string $identifier): bool {
$tenant = Tenant::query()->where('tenant', '=', $identifier)->first();
if ($tenant) {
$this->setTenant($tenant);
return true;
}
return false;
}
public function checkSubdomain(string $identifier) : bool {
$subdomain = Subdomain::query()->where('subdomain', '=', $identifier)->first();
if ($subdomain) {
if ($subdomain->nested_tenants) {
// tenant not found yet so do not set tenant
return true;
} else {
return $this->loadTenant($identifier);
}
}
return false;
}
}
Service Provider
class TenantServiceProvider extends ServiceProvider
{
public function register()
{
$manager = new TenantManager;
$this->app->instance(TenantManager::class, $manager);
$this->app->bind(Tenant::class, function() use ($manager) {
$tenant = $manager->getTenant();
if ($tenant === null) {
return new Tenant;
}
return $manager->getTenant();
});
}
}
Login Controller:
class LoginController extends Controller
{
public function __construct()
{
$this->middleware('guest')->except('logout');
}
...
public function login(Request $request, Tenant $tenant) {
$request->validate([
'email' => ['required', 'email', 'max:255'],
'password' => ['required'],
]);
$credentials = $request->only('email', 'password');
$credentials['status'] = 1;
if ($tenant->id) {
$credentials['tenant_id'] = $tenant->tenant;
}
if (Auth::attempt($credentials)) {
return redirect()->intended('home');
}
return Redirect::to('login')->withSuccess('Login Failed! You entered invalid credentials');
}
...
}
Issues
My main concern is that I don't feel like this is the best approach to keeping track of the tenant. I need it so that once the tenant is set I can use it throughout the application, without always first checking if the user is authenticated first - to then get the tenant. I am currently adding Tenant $tenant to the controller methods where I need tenant related data, but is a there better way of going about this?
Any advice on how I could improve my current set up would be helpful.
I think you should implement Traits to add tenant constraints for example:
in models:
BelongsToTenantModelTrait{
public static function bootBelongsToTenantModelTrait(){
static::addGlobalScope(function ($model){
model->where('tenant_id',auth()->user()->tenant->id);
//Or any similar logic
});
}
and other traits to controllers if needed.
You may also add middlewares like AuthTenant if needed as well.
I think this way should decouple the tenant-related logic as much as possible.
Let me know what you think.

Implement authorization in Laravel using external model

I want to protect my routes with authorization levels in laravel - client users have access to one group while admin gets access to a separate group. However, the laravel API I'm creating runs concurrent to the existing legacy app which uses its own Users class, rather than the pre rolled eloquent Users model that all the docs use.
So far I haven't been able to determine how best to create this custom authorization middleware.
Basically I'd like to do something like this:
Route::group(['middleware' => 'custom_auth', function() {
Route::get('/' function() {
return "Hello World";
}
}];
//where 'custom_auth' points to something like
function isAdmin() {
if (Core\User->check_is_admin()) {
return true;
} else {
return false;
}
}
Kind of an open-ended question, I know, so any links to blogs/docs/videos would be appreciated. Thanks!
Solved:
I added custom middleware to App\Http\Middleware that copied the structure of the prebuilt Middleware\Authenticate.php file and uses the custom isAdmin() method.
Make sure to include in HTTP\Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth_admin' => \App\Http\Middleware\AuthenticateAdmin::class, // <- New Middleware
...]
Then in my Routes\admin_api
Route::group(['middleware' => 'auth_admin'], function () {
Route::get('/', function () {
return "Hello World";
});
This all works!

How to Use Permissions in Sentry for Laravel application

I need to use Sentry 2.1 in a Laravel application, I read this document
https://cartalyst.com/manual/sentry/2.1
what I really need to have is some groups and assign some permissions to each group and then assign those groups to the users.
take this as an example (which I took from the same link):
I register a user with following detaiks
Sentry::register(array(
'email' => 'john.doe#example.com',
'password' => 'foobar',
'activated' => true,
));
Then I register a group with the following details:
$group = Sentry::createGroup(array(
'name' => 'Moderator',
'permissions' => array(
'admin' => 1,
'writers' => 1,
),
));
And then I assigned the group to the user
The Question:
Can someone provide me with a piece of code that helped me through how I should modify routes.php and add filters to it, so that the filters will apply on permissions and not the groups.
Route::group(array('before' => 'admin'), function()
{
Route::controller('admin','adminController');
});
Route::group(array('before' => 'mod'), function()
{
Route::controller('cruds','crudController');
});
For example users with admin permissions can only see the adminController links
Checking permissions is done via the Sentry hasAccess() method. You can either create multiple filters to take specific actions for different permission checks, or you can use a generic filter which takes the permission as a parameter and check on that. Below is a generic "hasAccess" filter, to which you pass the permission for which to check.
Filter:
Route::filter('hasAccess', function ($route, $request, $value) {
try {
// get the logged in user
$user = Sentry::getUser();
// check the user against the requested permission
if (!$user->hasAccess($value)) {
// action to take if the user doesn't have permission
return Redirect::home();
}
} catch (Cartalyst\Sentry\Users\UserNotFoundException $e) {
// action to take if the user is not logged in
return Redirect::guest(route('login'));
}
});
Routes:
Route::group(array('before' => 'hasAccess:admin'), function() {
Route::controller('admin','adminController');
});
Route::group(array('before' => 'hasAccess:mod'), function() {
Route::controller('cruds','crudController');
});

testing filters in laravel 4

I'm new to laravel. I am trying to test that authentication works for my website and I want to test the authentication process in a test case. I create a in-memory sqlite database, I create a new User and use ->save() method of eloquent to store it in the database. I have setup an authentication filter which checks for the username in the database and depending on that it either allows the user to login or returns "invalid credentials"
// my UserTest.php file :
class UserTest extends TestCase {
public function testUsernameIsNotRequired()
{
// Create a new User
$user = new User;
$user->username = "phil#ipbrown.com";
$user->password = "123456";
//$user->password_confirmation = "password";
// User should save
$this->assertTrue($user->save());
// Save the errors
$password = $user->getAuthPassword();
// There should be 0 error
$this->assertEquals("123456",$password);
$this->seed();
$this->be($user);
$this->assertTrue(Redirect::route('authFilter'));
}
}
just to let you know that the in-memory db is lost once the test is complete as all the connections to it are lost so I want to check that the user that I saved to my db is properly inserted and second I want to check if I can login to my website using the information of this new user.
// my filters.php file :
Route::filter('auth', function()
{
if (Auth::guest()) return Redirect::guest('login');
});
Route::filter('auth.basic', function()
{
return Auth::basic('username');
});
Route::filter('guest', function()
{
if (Auth::check()) return Redirect::to('/');
});
Route::filter('csrf', function()
{
if (Session::token() != Input::get('_token'))
{
throw new Illuminate\Session\TokenMismatchException;
}
});
I was trying to attach a filter to a route so that I can redirect to the route during my test and which calls the auth.basic filter so I can test my filter, I know Im doing a lot of things wrong so please feel free to correct any mistakes that you come accross
my routes.php file :>
Route::get('/', function()
{
return View::make('hello');
});
Route::get('/authtest', array('as'=>'/authtest','before' => 'auth.basic', function()
{
return View::make('hello');
}));
Route::group(array('prefix' => 'api/v1', 'before' => 'auth.basic'), function()
{
Route::resource('url', 'UrlController');
});
Route::get('authFilter', array('as'=>'authFilter','before' => 'auth.basic', function()
{
return Auth::basic('username');
}));
I also have a uri controller which has all the pages for my website
this is what I followed to create my uri controller for the moment
I need a test case that creates a user stores it into the in-memory database and then authenticates using that users information. If any one knows laravel testing for filters please let me know I looked up the documentation for testing filters but I guess it is not well documented.
thank you
Filters are disabled in tests on Laravel 4.
You can use Route::enableFilters() in your test to force the filters on.
You can read up on the various discussions about why/why not to test filters;
https://github.com/laravel/framework/issues/344
https://github.com/laravel/framework/issues/766
http://forums.laravel.io/viewtopic.php?id=7404
https://github.com/laravel/framework/issues/682
You can keep most of this in the unit test.
public function testMyFilter() {
// Set up route with filter.
Route::get('testFilter', ['before' => 'myfilter', function()
{
return 'Hello World';
}]);
// Enable filters.
Route::enableFilters();
// Be a user.
$this->be($this->user);
// Make a request that will use the filter.
$response = $this->client->request('GET', 'testFilter');
// Check the response.
$this->assertResponseOk();
}

Categories