I'm trying to figure out how to only allow one session per user.
So if someone tries to log in when he already logged in his user account, the first session will be destroyed and will be logged out to allow the current session only.
I'm following this: How to keep single session per user in Laravel . But i don't know where I should put these lines of codes:
/**
* Swap a user session with a current one
*
* #param \App\User $user
* #return boolean
*/
protected function swapUserSession($user)
{
if (!($user instanceof \App\User)) {
return false;
}
$new_session_id = Session::getId(); //get new session_id after user sign in
$last_session = Session::getHandler()->read($user->last_session_id); // retrive last session
if ($last_session) {
Session::getHandler()->destroy($user->last_session_id);
}
$user->last_session_id = $new_session_id;
$user->save();
return true;
}
I'm currently using Laravel 5.1, so The only controller I can find for the Auth is AuthController.php but it says to put it on the LoginController.php
If you're using the file driver you can add the following method to App\Http\Controllers\Auth\AuthController.php (tested in Laravel 5.2)
/**
* Only allow one concurrent session per user
*
* #param Request $request
* #param User $user
* #return Response
*/
protected function authenticated(Request $request, User $user)
{
Session::put('user_id', $user->id);
$files = array_diff(scandir(storage_path('framework/sessions')), array('.', '..', '.gitignore'));
foreach ($files as $file) {
$filepath = storage_path('framework/sessions/' . $file);
$session = unserialize(file_get_contents($filepath));
if ($session['user_id'] === $user->id && $session['_token'] !== Session::get('_token')) {
unlink($filepath);
}
}
return redirect()->intended($this->redirectPath());
}
If you're running off of the latest laravel 5.3.15 or higher I would recommend checking out the following link I've posted about this issue I've ran into. Took me some time to figure out but I did and the fix is really simple.
First (assuming you've installed laravel 5.3 cleanly):
Get sessions table up and running-
Go to the session configuration file stored at config/session.php and scroll down to the driver configuration.
By default it should be set to 'file'.
Change this setting to 'database'. (Provided you want to store sessions in a database)
In your console, in the project directory, perform:
php artisan session:table
php artisan migrate
Then in the following link scroll down to the last comment and follow the steps to fix the session issue.
https://github.com/laravel/framework/issues/15821
Related
I am overwriting session.timeout value in one of the middleware (for Laravel web app) but it doesn't seem to be affecting in terms of timing out a session. Though if I debug it shows value I have overwritten.
Config::set('session.lifetime', 1440);
default value is as following:
'lifetime' => 15,
Website that I am working on has very short session lifetime for most of the users but for selected users I want to provide extended session lifetime.
It seems the only way to accomplish a dynamic lifetime value, is by setting the value in middleware, before the session gets initiated. Otherwise its too late, as the application SessionHandler will have already been instantiated using the default config value.
namespace App\Http\Middleware;
class ExtendSession
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, $next)
{
$lifetime = 2;
config(['session.lifetime' => $lifetime]);
return $next($request);
}
}
Then in the kernel.php file, add this class prior to StartSession.
\App\Http\Middleware\ExtendSession::class,
\Illuminate\Session\Middleware\StartSession::class,
Here is what worked for me (using Laravel 5.6 or 5.5) to let a user choose session duration at login time.
Editing the lifetime of the session in Auth controller doesn't work because by then the session is already started. You need to add middleware that executes before Laravel runs its own "StartSession" middleware.
One way is to create a cookie to store the user's lifetime length preference and use that value when setting the session expiration on each request.
New file: app/Http/Middleware/SetSessionLength.php
namespace App\Http\Middleware;
use Illuminate\Support\Facades\Cookie;
class SetSessionLength {
const SESSION_LIFETIME_PARAM = 'sessionLifetime';
const SESSION_LIFETIME_DEFAULT_MINS = 5;
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, $next) {
$lifetimeMins = Cookie::get(self::SESSION_LIFETIME_PARAM, $request->input(self::SESSION_LIFETIME_PARAM)); //https://laravel.com/api/6.x/Illuminate/Support/Facades/Cookie.html#method_get
if ($lifetimeMins) {
Cookie::queue(self::SESSION_LIFETIME_PARAM, $lifetimeMins, $lifetimeMins); //https://laravel.com/docs/6.x/requests#cookies
config(['session.lifetime' => $lifetimeMins]);
}
return $next($request);
}
}
Modify Kernel: app/Http/Kernel.php
Add \App\Http\Middleware\SetSessionLength::class, right before \Illuminate\Session\Middleware\StartSession::class,.
Modify Config: config/session.php
'lifetime' => env('SESSION_LIFETIME', \App\Http\Middleware\SetSessionLength::SESSION_LIFETIME_DEFAULT_MINS),
Modify: resources/views/auth/login.blade.php
To let the user chose their preferred number of minutes, add a dropdown of minutes, such as starting with <select name="{{\App\Http\Middleware\SetSessionLength::SESSION_LIFETIME_PARAM}}">. Otherwise, change SetSessionLength.php above not to pull from $request->input but retrieve from somewhere else, such as a database record for that user.
The problem occurs because the session has already started, and after that you are changing session lifetime configuration variable.
The variable needs to be changed for current request, but the user already has a session with lifetime specified.
You have to change your login method. And do following steps:
See if user exists in database
If yes, and he is user who needs longer session lifetime, run config(['session.lifetime' => 1440]);
log user in
I recommend using helper to change config on the fly.
config(['session.lifetime' => 1440]);
A really strange issue here. I had a Laravel 5.2 application which work perfectly. Then I update to Laravel 5.3 to use the new broadcasting features and I face a big issue.
When I update the data (with my application forms or directly in my database) the views are note updated properly. I try to clear cache, views and config but nothing change... I need to go to some others pages and the data finish by appear...
I have a Campaign model and a page which list campaigns. When I remove an entry directly in the database, the list doesn't change in front. Also when I use debugging functions like dd results tell me that data haven't changed...
Is there someone else which faced the same problem ?
I've followed the migration guide to update my 5.2 to 5.3, maybe I forgot something...
Here a piece of my .env file :
DB_CONNECTION=mysql
BROADCAST_DRIVER=redis
CACHE_DRIVER=array
SESSION_DRIVER=file
QUEUE_DRIVER=database
Thanks !
Thank you for sharing this questions.
Laravel successfully upgraded to version 5.3 and there are some deprecations and application service provider and also some new feature like passport are added.
Your problem is with view. As per my knowledge, you need to remove arguments from your "boot" method which are written in EventServiceProvider, RouteServiceProvider, AuthServiceProvider which are available on app/provider/remove_the_arguments_from_boot_method_given_file
In Laravel 5.2:
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
}
But in Laravel 5.3:
public function boot()
{
parent::boot();
}
Kindly refer Laravel 5.3 docs
I hope, this works for you.
Thanks and regards.
Never store full model in session, it can lead to old data displayed in the application !
After a day of search and refactoring I found what was my original problem !
It's a simple session() statement which cause my application to display invalid data.
History
The dashboard display a list of campaigns which are linked to a client. A user can manage multiple clients so I put the current client in session to know which one is currently used.
The mistake here is that I put the entire client model in session so when I read the session and retrieve data, all the relationships are retrieved too.
The client is the central point to access data in my application. I retrieve the campaigns linked to my client and everything is related to it.
Here the vicious function :
/**
* Retrieve the current client instance when the user is connected
* #return App\Client|null
*/
protected function retrieveCurrentClient()
{
$client = null;
if (Gate::allows('manage-clients')) {
if (null === $client = session('currentClient')) {
$client = Client::all()->first();
session(['currentClient' => $client]);
}
} elseif (Auth::guard()->check()) {
$client = Auth::guard()->user()->client;
}
return $client;
}
In fact the problem appeared when I dig around the Gate definition. If I remove them my application starts working again...
Solution
I just change the function to store in session client id instead of the full Model. Then I retrieve fresh data in each page of my application.
/**
* Retrieve the current client instance when the user is connected
* #return App\Client|null
*/
protected function retrieveCurrentClient()
{
$client = null;
if (Gate::allows('manage-clients')) {
if (null === $client_id = session('client_id')) {
$client = Client::all()->first();
session(['client_id' => $client->id]);
} else {
$client = Client::findOrFail($client_id);
}
} elseif (Auth::guard()->check()) {
$client = Auth::guard()->user()->client;
}
return $client;
}
Don't know if it can help someone else to avoid that mistakes but happy to have found an answer !
I do have a UserController and User Model in my Laravel 5 source.
Also there is one AuthController is also Present (shipped prebuilt with laravel source).
I would like to query data from db in my blades making use of Eloquent Models.
However, Neither in my User Model (Eloquent ) nor in any of the controller, the user() method is defined. even then, I could use it in my blade by accessing it from Auth class. why?
For example,
in my blade, {{ Auth::user()->fname }} works. it retrieve the data fnamefrom my users table and echo it.
What is the logic behind it, and can i emulate the same for other db tables such as tasks?
Whenever you do it automatically or manually some like this
if (Auth::attempt(['email' => $email, 'password' => $password]))
{
}
The selected User's Data will be stored in the storage/framework/sessions
It will have data something like
a:4:{s:6:"_token";s:40:"PEKGoLhoXMl1rUDNNq2besE1iSTtSKylFFIhuoZu";s:9:"_previous";a:1:{s:3:"url";s:43:"http://localhost/Learnings/laravel5/laravel";}s:9:"_sf2_meta";a:3:{s:1:"u";i:1432617607;s:1:"c";i:1432617607;s:1:"l";s:1:"0";}s:5:"flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}
The above sessions file doesn't have any data and it will have the data such as user's id, url, token in json format.
Then whenever you call the {{ Auth::user()->fname }} Laravel recognises that you're trying to fetch the logged in user's fname then laravel will fetch the file and get the user's primary key and refer it from the user's table from your database. and you can do it for all coloumns of the users table that you have.
You can learn more about it here
This user function is defined under
vendor/laravel/framework/src/Illuminate/Auth/Guard.php
with following content :
/**
* Get the currently authenticated user.
*
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
if ($this->loggedOut) return;
// If we have already retrieved the user for the current request we can just
// return it back immediately. We do not want to pull the user data every
// request into the method because that would tremendously slow an app.
if ( ! is_null($this->user))
{
return $this->user;
}
$id = $this->session->get($this->getName());
// First we will try to load the user using the identifier in the session if
// one exists. Otherwise we will check for a "remember me" cookie in this
// request, and if one exists, attempt to retrieve the user using that.
$user = null;
if ( ! is_null($id))
{
$user = $this->provider->retrieveById($id);
}
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
$recaller = $this->getRecaller();
if (is_null($user) && ! is_null($recaller))
{
$user = $this->getUserByRecaller($recaller);
if ($user)
{
$this->updateSession($user->getAuthIdentifier());
$this->fireLoginEvent($user, true);
}
}
return $this->user = $user;
}
this Guard.php has more functions defined in it which we use every now and then without even knowing where they are coming from
It works because Laravel comes with decent authentication.
Auth is the authentication library and has plenty of features like this, check out the documentation!
Is there a way to check if a user already has a valid session on a different machine?
What I want to do is when a user logs in, destroy an other sessions which they may already have, so that if they forget to logout from a computer say on campus or at work, and then they log in at home, it will destroy those other 2 sessions so they are no longer logged in?
Facebook employs this in some way.
My only thoughts so far is something to this effect:
$user = User::find(1); // find the user
Auth::login($user); // log them in
Auth::logout(); // log them out hoping that it will destroy all their sessions on all machines
Auth::login($user); // log them in again so they have a valid session on this machine
I have not had the chance to test this, and I do not know if Auth::login($user); will destroy all sessions for that user, or only the current one.
Thanks!
You can save a session_id within a user model, so that:
When logout event is fired (auth.logout) you would clear it.
When new logging event is fired you can check if attribute session_id is not null within the user model.
If it's not - destroy previous session by:
Session::getHandler()->destroy($user->session_id);
$user->session_id = Session::getId();
Hope that would help!
I realise this is an old question, but there is now a method in laravel 5.6 that does exactly this, so it may be useful for someone coming to this later. You can also retro-fit this method to earlier versions of laravel very easily.
See the docs at https://laravel.com/docs/5.6/authentication#invalidating-sessions-on-other-devices
I had the same use case as you (log out all other devices on log-in). I overrode the default login method to add my own custom logic (first copying the default login method from vendor/laravel/framework/src/illuminate/Foundation/Auth/AuthenticatesUsers.php)
In that method, there is the line if ($this->attemptLogin($request)) - within this, before the return statement, add your call to logoutOtherDevices, as below
if ($this->attemptLogin($request)) {
//log out all other sessions
Auth::logoutOtherDevices($request->password); //add this line
return $this->sendLoginResponse($request);
}
Also ensure you have un-commented the Illuminate\Session\Middleware\AuthenticateSession middleware in your app/Http/Kernel.php, as per the docs
(note that I haven't tested the above code as I was using an older version of laravel that doesn't have this method, see below). This should work in 5.6 though.
Older Laravel versions
I was actually using laravel 5.5, so didn't have access to this handy method. Luckily, it's easy to add.
I opened a laravel 5.6 project and copied the logoutOtherDevices method from vendor/laravel/framework/src/illuminate/Auth/SessionGuard.php - for reference I have pasted below
/**
* Invalidate other sessions for the current user.
*
* The application must be using the AuthenticateSession middleware.
*
* #param string $password
* #param string $attribute
* #return null|bool
*/
public function logoutOtherDevices($password, $attribute = 'password')
{
if (! $this->user()) {
return;
}
return tap($this->user()->forceFill([
$attribute => Hash::make($password),
]))->save();
}
I then copied this into my LoginController - it could go somewhere else of your choice, but I've put it here for ease / laziness. I had to modify it slightly, as below ($this->user() becomes Auth::user())
/**
* Invalidate other sessions for the current user.
* Method from laravel 5.6 copied to here
*
* The application must be using the AuthenticateSession middleware.
*
* #param string $password
* #param string $attribute
* #return null|bool
*/
public function logoutOtherDevices($password, $attribute = 'password')
{
if (! Auth::user()) {
return;
}
return tap(Auth::user()->forceFill([
$attribute => Hash::make($password),
]))->save();
}
I can then call this method in my login method, as specified earlier in my answer, with a slight adjustment - $this->logoutOtherDevices($request->password);
If you want to test this locally, it seems to work if you open your site on a normal and an incognito window. When you log in on one, you'll be logged out on the other - though you'll have to refresh to see anything change.
I hope you will see this job:
Session::regenerate(true);
a new session_id be obtained.
This may not be the best answer, but first thing that came to my mind was lowering the timeout on the session.
In app->config->session.php there's a setting for both lifetime and expire_on_close (browser).
I'd try looking into that for now, and see if someone else comes up with something better.
I am building my first Laravel 4 Application (PHP).
I find myself needing to call somthing like this often in most of my Models and Controllers...
$this->user = Auth::user();
So my question is, is calling this several times in the application, hitting the Database several times, or is it smart enough to cache it somewhere for the remainder of the request/page build?
Or do I need to do it differently myself? I glanced over the Auth class but didnt have time to inspect every file (16 files for Auth)
Here is the code for the method Auth::user().
// vendor/laravel/framework/src/Illuminate/Auth/Guard.php
/**
* Get the currently authenticated user.
*
* #return \Illuminate\Auth\UserInterface|null
*/
public function user()
{
if ($this->loggedOut) return;
// If we have already retrieved the user for the current request we can just
// return it back immediately. We do not want to pull the user data every
// request into the method becaue that would tremendously slow the app.
if ( ! is_null($this->user))
{
return $this->user;
}
$id = $this->session->get($this->getName());
// First we will try to load the user using the identifier in the session if
// one exists. Otherwise we will check for a "remember me" cookie in this
// request, and if one exists, attempt to retrieve the user using that.
$user = null;
if ( ! is_null($id))
{
$user = $this->provider->retrieveByID($id);
}
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
$recaller = $this->getRecaller();
if (is_null($user) and ! is_null($recaller))
{
$user = $this->provider->retrieveByID($recaller);
}
return $this->user = $user;
}
To me, it looks like it will get the user from the database only once per request. So, you can call it as many times as you want. It will only hit the DB once.
Auth::user() only hits the DB once, so it's not a problem invokes it many times. Btw, you can cache useful information of the user that you want to access frequently.