Laravel session regenerate() and invalidate() functions - php

I am implementing some custom auth functionality in to my application, which is built in Laravel v8. I have been looking at Laravel Breeze to see how it is has been implemented there.
These are the relevant functions from Laravel Breeze (https://github.com/laravel/breeze/blob/1.x/stubs/default/App/Http/Controllers/Auth/AuthenticatedSessionController.php):
class AuthenticatedSessionController extends Controller
{
/**
* Handle an incoming authentication request.
*
* #param \App\Http\Requests\Auth\LoginRequest $request
* #return \Illuminate\Http\RedirectResponse
*/
public function store(LoginRequest $request)
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
/**
* Destroy an authenticated session.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request)
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
So you will notice:
In the store() function, which is called during Login, it does $request->session()->regenerate();
In the destroy() function, which is called during Logout, it does $request->session()->invalidate();
In my application custom auth code, I have applied the same implementation in my login and logout actions.
What I have found is, when I logout, it deletes the existing session file inside storage/framework/sessions but then creates another one.
Then when I login, it creates a brand new session file. This essentially means the folder gets full of session files.
Does anyone know the reason why it is implemented this way? I would have thought logout would just delete the session file without creating a new one?

That is the normal behavior of PHP and it is not specific to Laravel.
In PHP by default, all sessions are files that are stored in a tmp directory which if you analyze the session.save_path value in php.ini file, you will see where it is. These files include the serialized session data that you access using $_SESSION keyword.
Laravel utilize basically the original session file store but acts a little bit different. Beside changing the directory they are saved, when you call the regenerate function it creates another session file and deletes the old one. You can see it the implementation Illuminate\Session\Store.php. The migrate function is used to delete the session and return new session id.
public function regenerate($destroy = false)
{
return tap($this->migrate($destroy), function () {
$this->regenerateToken();
});
}
For the invalidate function it deletes the session file actually. If you inspect the implementation, the invalidate calls for migrate method, it calls a destroy method with session id as input and this function simply deletes the session file. But soon after it deletes the file, it needs to create a new session file, why? Because the logged out user needs a new session id so we can track them.
public function invalidate()
{
$this->flush();
return $this->migrate(true);
}
Laravel Session Cleanup
Laravel has a garbage cleanup functionality which runs randomly and deletes the session files that are not valid. What does it mean randomly? Well the cleanup operation is triggered based on a randomness and traffic. So in each request Laravel checks the odd of triggering the clean or not, and the odds are 2 out of 100 by default. This means if applications receives 50 requests, there is a high chance that it will trigger this cleanup.
So if you have a high traffic, there is a high chance that the session directory will be cleared at short intervals, which is quire cool since it always makes sure that the specified directory does not get over populated when visiting users increase.
By the way if you want to act aggressively and delete on a higher chance, you can change the lottery odds in the config\session.php file:
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],

Related

Logout does not expire session tokens

We recently got our Laravel 5.6 application penetration tested and one of the issues which were flagged was the expiration not being set correctly on Logout. The AuthenticatesUsers trait calls the invalidate method on the session which basically flushes the session data and regenerates the ID but doesn't set expiration to it.
According to the report, if an attacker can obtain a valid session token, they will be able to hijack the affected user’s account. The user logging off will not invalidate the attacker’s session.
Any pointers here would be of great help.
Thanks
/**
* Log the user out of the application.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function logout(Request $request)
{
$this->guard()->logout();
$request->session()->invalidate();
return redirect('/');
}
Laravel 5.6 added an Auth::logoutOtherDevices() method for this purpose:
https://laravel.com/docs/5.7/authentication#invalidating-sessions-on-other-devices
https://laracasts.com/series/whats-new-in-laravel-5-6/episodes/7
https://github.com/laravel/framework/issues/16311

Laravel customized session.lifetime at user level

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]);

Laravel, 2 projects in 2 domains same session

I'm creating 2 projects in 2 diferent domains domain1.tld and domain2.tld.
The domain1.tld is the main event producer page and the domain2.tld is one of its events. I want to share the same sessions (they actually share the same database and the same apache server). I tried to change the session driver to "database" and create a session table, but nothing happens, if i'm log in domain1.tld nothing happens in domain2.tld.
I really have searched in the net but i have found nothing
you can't do this in your way...
when you set session, a cookie set in browser for track stored session in server side.
if you want to share session between two domain you should share cookie between to site bot you can not do it (you can do it just in sub domains of ONE domain)
but there is a little hack :
The easiest work-around is to pass login/credential information from website A to website B and have website B set a seperate cookie. For example, after logging into website A you could have them quickly redirected to website B with an encrypted querystring. Website B could then read the information, set its own cookie, and redirect the user back to site A.
It's messy but possible.
Step 1: Set Session Driver for Shared Session Data
First, set your session driver to a database or cache that is shared across both domains. Your session driver cannot be file
Step 2: Implement Cross-Domain Session IDs
Session ids are passed around by cookies in Laravel. Since your websites are on different domains the session cookie does not transfer over. One way to solve this is to append them to the query string of all your requests like so: domain2.tld/?session_token=abcds2342
Within your code there must be some login that detects a session and then query the database/cache (your session driver) for a result. If a result is found, you set the session ID manually and start the session:
session_id('abcds2342');
session_start();
Be careful to check both the IP address and the session ID
to prevent people from guessing someone elses SessionID and thus
logging in as another person
Step 2A: To do this you can implement a custom middleware that overrides StartSession. This middleware should override getSession and before it checks for session_id in cookie, check if we have a token present in the Request. Sample code below:
<?php
namespace App\Http\Middleware;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Http\Request;
use App\SessionShare;
use Closure;
class StartSessionWithSharer extends StartSession
{
/**
* Get the session implementation from the manager.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Session\SessionInterface
*/
public function getSession(Request $request)
{
$session = $this->manager->driver();
/**
* Check if we can find a valid session token from saved records
*/
if($request->get('session_token') && !empty($request->get('session_token'))) {
$sessionShare = SessionShare::valid()->whereToken($request->get('session_token'))->first();
if($sessionShare)
$session_id = $sessionShare->session_id;
}
/**
* Fallback to session in browser
*/
if(!isset($session_id) || !$session_id)
$session_id = $request->cookies->get($session->getName());
$session->setId($session_id);
return $session;
}
}
Step 2B: Then create a custom service provider to override SessionServiceProvider like so:
<?php namespace App\Providers;
class CustomSessionServiceProvider extends \Illuminate\Session\SessionServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerSessionManager();
$this->registerSessionDriver();
$this->app->singleton('App\Http\Middleware\StartSessionWithSharer');
}
}
And then remove the old SessionServiceProvider from config/app.php and instead use above.
Step 2C: Then create your App\SessionShare model for the table to store session IDs. Also, the above code doesn't take care of checking IP address so you would have to add that in to make it secure and prevent brute force attacks
Step 2D: Oh and finally don't forget to append the get parameter for session_token for all your requests
Note that the above implementation is for a database session driver. Of course, you can do this for a cache driver too. The only thing that would change is the model implementation (step 2C) to validate the session

Laravel - destroy existing sessions on login

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.

Does PHP's Laravel 4 hit the Database on every call to the Auth class?

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.

Categories