Bottom line:
How to logout the user on session time out?
Detailed Question:
I have a Laravel 5.6.* application and the project demands the user to logout whenever the user is idle. I have tried the solutions that are given here, but none of them worked for me.
Then I stumbled upon this post:
https://laravel-tricks.com/tricks/session-timeout-for-logged-in-user and made my way through it to no success.
What I want:
Logout the user automatically on session timeout. Before logging out, set is_logged_in attribute to false or 0 on the Users table. How do I achieve this?
Code that I have tried so far:
session.php
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => false,
SessionTimeOut.php Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use App\Traits\CacheQueryResults;
class SessionTimeOut
{
use CacheQueryResults;
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
// session()->forget('lastActivityTime');
if (! session()->has('lastActivityTime')) {
session(['lastActivityTime' => now()]);
}
// dd(
// session('lastActivityTime')->format('Y-M-jS h:i:s A'),
// now()->diffInMinutes(session('lastActivityTime')),
// now()->diffInMinutes(session('lastActivityTime')) >= config('session.lifetime')
// );
if (now()->diffInMinutes(session('lastActivityTime')) >= (config('session.lifetime') - 1) ) {
if (auth()->check() && auth()->id() > 1) {
$user = auth()->user();
auth()->logout();
$user->update(['is_logged_in' => false]);
$this->reCacheAllUsersData();
session()->forget('lastActivityTime');
return redirect(route('users.login'));
}
}
session(['lastActivityTime' => now()]);
return $next($request);
}
}
Kernel.php
/**
* The application's route middleware groups.
*
* #var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SessionTimeOut::class,
],
];
You are comparing session lifetime same as in middleware.
That Means when session will expire, your middleware will not(never) called.And user will move to login page.
If you want to save entry in Database, You can set long-time session lifetime, and in middleware use your custom time to logout.
Change in config/session.php
'lifetime' => 525600, // for one year, it will be in minute, use as you want.
Change in middleware as below, log out after two hours.
if (now()->diffInMinutes(session('lastActivityTime')) >= (120) ) { // also you can this value in your config file and use here
if (auth()->check() && auth()->id() > 1) {
$user = auth()->user();
auth()->logout();
$user->update(['is_logged_in' => false]);
$this->reCacheAllUsersData();
session()->forget('lastActivityTime');
return redirect(route('users.login'));
}
}
By this way your session will not expire automatically and you can manipulate data.
Need to update the database before logout. Because after logout can't perform $user->update(). so that try following way:
if (auth()->check() && auth()->id() > 1) {
$user = auth()->user();
$user->update(['is_logged_in' => false]);
$this->reCacheAllUsersData();
session()->forget('lastActivityTime');
//Add Logout method here..
auth()->logout();
return redirect(route('users.login'));
}
please check less than 120 in middleware,ex 115 or 119 in below if condition and then check it
if (now()->diffInMinutes(session('lastActivityTime')) == config('session.lifetime')) {
....
}
Related
I've implemented a "custom" login setup for our application as we are using the X-Forwarded-User from our proxy (NB - Internal low-security application before we do down that route!)
Unfortunately, it seems that every time we load the page, the user isn't actually logged in. They are logged in as soon as I do Auth::login, but as soon as the page reloads and I do an Auth::user check, they are not logged in.
As you can see here, this is run via middleware so it's run on every request, but I'm a little confused why I am having to do a login every time the page loads and the user isn't "really" getting logged in.
<?php
namespace App\Http\Middleware;
use App\User;
use Closure;
use Illuminate\Support\Facades\Auth;
class ValidateForwardedUser
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($request->header('X-Forwarded-User')) {
$forwardedUser = $request->header('X-Forwarded-User');
// This is always empty
print_r("Logged in user before auth = ".Auth::user()."<br>");
if (!Auth::check() || Auth::user() != $forwardedUser) {
if (User::where('email', '=', $forwardedUser)->first()) {
$user = User::where('email', '=', $forwardedUser)->first();
Auth::login($user);
} else {
$user = new User();
$user->email = $forwardedUser;
$user->save();
Auth::login($user);
}
} else {
// We should never hit this, but we throw an exception so it's debugable if we do!
throw new \Exception('ValidateForwardedUser Exception #1');
}
// This is always populated
print_r("Logged in user before auth = ".Auth::user()."<br>");
return $next($request);
} else {
// We should never hit this, but we throw an exception so it's debugable if we do!
throw new \Exception('ValidateForwardedUser Exception #2');
}
}
}
All routes use the "web" middleware which is shown below (Specifically the problem here appears to be with ValidateForwardedUser)
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\App\Http\Middleware\ValidateForwardedUser::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\BrowserFilter::class,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
Edit: After some debugging, every refresh of the page creates a new session. This doesn't seem right and might be part of the issue?
Although login was working every time (but not persisting between page reloads), updating my User model path fixed my issue.
For some reason or another, I'd previously changed my User model to App\User.
Moving it back to App\Models\User (and updating all relevant references) made solved my issue.
I'm currently making an app on Laravel (5.3) and require a list of all logged-in users. I was originally going to store them in MySQL but was advised that Redis would be better for the job. Once I looked at Redis docs I was going to store all the users in a set, but then realised you can't set an expire time on individual members, and so opted for namespaced strings instead.
I have written some code that I believe is functioning correctly, but would like advice on improving it/fixing any problems there might be.
So first, here are the two functions I added in LoginController.php
// Overriding the authenticated method from Illuminate\Foundation\Auth\AuthenticatesUsers
protected function authenticated(Request $request, $user)
{
$id = $user->id;
// Getting the expiration from the session config file. Converting to seconds
$expire = config('session.lifetime') * 60;
// Setting redis using id as namespace and value
Redis::SET('users:'.$id,$id);
Redis::EXPIRE('users:'.$id,$expire);
}
//Overriding the logout method from Illuminate\Foundation\Auth\AuthenticatesUsers
public function logout(Request $request)
{
// Deleting user from redis database when they log out
$id = Auth::user()->id;
Redis::DEL('users:'.$id);
$this->guard()->logout();
$request->session()->flush();
$request->session()->regenerate();
return redirect('/');
}
Next I wrote Middleware called 'RefreshRedis' in order to refresh the expiration on the Redis when the user does something that refreshes their session.
public function handle($request, Closure $next)
{
//refreshing the expiration of users key
if(Auth::check()){
$id = Auth::user()->id;
$expire = config('session.lifetime') * 60;
Redis::EXPIRE('users:'.$id,$expire);
}
return $next($request);
}
I then registered the Middleware in $middlewareGroups just after the StartSession Middleware
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\RefreshRedis::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
In order to get a list of all the users I used a modified version of the function found in this thread.
class Team extends AbstractWidget
{
/**
* Treat this method as a controller action.
* Return view() or other content to display.
*/
public function run()
{
//Find all logged users id's from redis
$users = $this->loggedUsers('users:*');
return view('widgets.team',compact('users'));
}
protected function loggedUsers($pattern, $cursor=null, $allResults=array())
{
// Zero means full iteration
if ($cursor==="0"){
$users = array ();
foreach($allResults as $result){
$users[] = User::where('id',Redis::Get($result))->first();
}
return $users;
}
// No $cursor means init
if ($cursor===null){
$cursor = "0";
}
// The call
$result = Redis::scan($cursor, 'match', $pattern);
// Append results to array
$allResults = array_merge($allResults, $result[1]);
// Get rid of duplicated values
$allResults = array_unique($allResults);
// Recursive call until cursor is 0
return $this->loggedUsers($pattern, $result[0], $allResults);
}
}
You can expire Redis set keys by using the [multi] command that would finalize certain Redis command(s) by using the [exec] command.
protected function authenticated(Request $request, $user)
{
$id = $user->id;
// Getting the expiration from the session config file. Converting to seconds
$expire = config('session.lifetime') * 60;
// In using the [multi] and [exec] command
$oLibRedis = $this->callRedisLibrary(); // Initialize your Predis\Client class on this method
$oLibRedis->multi();
$oLibRedis->set('users:'.$id, $id);
$oLibRedis->expire('users:'.$id, $expire);
$oLibRedis->get('users:'.$id);
$aResults = $oLibRedis->exec();
return $aResults
}
from which it would generate the ff. results
array(3) { [0]=> object(Predis\Response\Status)#224 (1) { ["payload":"Predis\Response\Status":private]=> string(2) "OK" } [1]=> int(1) [2]=> string(5) "admin" }
The results will show up as an array based on what commands you have called so far on Redis (e.g set, expire and get commands)
By the time you wanted to check if the session is expired or not from another controller. You can use the [ttl] command in Redis.
$sRedisUserKey = $oLibRedis->ttl($sSampleUserKey); // int(7184)
Try to refresh the page to check the countdown for it. Hope this helps for you
I use Laravel 5.2, and I want to know how to force a user to log out by id. I'm building an admin panel with the option to deactivate a specific user that is currently logged in to the web application. Laravel gives you this option for a current user.
Auth::logout()
But I don't want to log out the current user, as I am an authenticated user. So I need to force log out of a specific user by its id. Just like when we log in a user with a specific id.
Auth::loginUsingId($id);
Is there something like the following?
Auth::logoutUsingId($id);
Currently, there's no straightforward way to do this; As the StatefulGuard contract and its SessionGuard implementation don't offer a logoutUsingId() as they do for login.
You need to add a new field to your users table and set it to true when you want a specific user to be logged out. Then use a middleware to check if the current user needs a force logout.
Here's a quick implementation.
1. Add a new field
Let's add a new field to users table migration class:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
// ...
$table->boolean('logout')->default(false);
// other fields...
});
}
// ...
}
Make sure you run php artisan migrate:refresh [--seed] after changing the migration.
2. Force logout middleware
Let's create a new middleware:
php artisan make:middleware LogoutUsers
Here's the logic to check if a user needs to be kicked out:
<?php
namespace App\Http\Middleware;
use Auth;
use Closure;
class LogoutUsers
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$user = Auth::user();
// You might want to create a method on your model to
// prevent direct access to the `logout` property. Something
// like `markedForLogout()` maybe.
if (! empty($user->logout)) {
// Not for the next time!
// Maybe a `unmarkForLogout()` method is appropriate here.
$user->logout = false;
$user->save();
// Log her out
Auth::logout();
return redirect()->route('login');
}
return $next($request);
}
}
3. Register the middleware in HTTP kernel
Open up the app/Http/Kernel.php and add your middleware's FQN:
/**
* The application's route middleware groups.
*
* #var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\LogoutUsers::class, // <= Here
],
'api' => [
'throttle:60,1',
'bindings',
],
];
It's untested code, but it should give you the idea. It'd be a good practice to add a couple of API methods to your User model to accompany with this functionality:
markedForLogout() : Checks user's logout flag.
markForLogout() : Sets user's logout flag to true.
unmarkForLogout() : Sets user's logout flag to false.
Then on the administration side (I suppose it's your case), you just need to call markForLogout() on the specific user model to kick him out on the next request. Or you can utilize the query builder to set the flag, if the model object is not available:
User::where('id', $userId)
->update(['logout' => true]);
It can be a markForLogoutById($id) method.
Related discussions
[Proposal] Log out users by ID
Multiple statements when logged users are deleted
Use the setUser to find a soluion
get current user
$user = Auth::user();
logout user you want to, by id
$userToLogout = User::find(5);
Auth::setUser($userToLogout);
Auth::logout();
set again current user
Auth::setUser($user);
I suggest you to override App\Http\Middleware\Authenticate handle function with custom check
if ($this->auth->user()->deleted_at) {
$this->auth->logout();
return redirect()->route('login');
}
The setup and below code I use it to logout a user and also lock his account(optional)using checklist, I did it through Ajax Request:
-In config/session.php, change to:
'driver' => env('SESSION_DRIVER', 'database'),
-run php artisan session:table
-run php artisan migrate //a session table is created.
-in .env file: change SESSION_DRIVER=file to SESSION_DRIVER=database
Now the coding part:
-In controller:
function ajaxCheckList(Request $request)
{
// \Log::info($request);
$user = \App\Models\User::findOrFail($request['user_id']);
$locked = 0;
if ($user->locked == 1) {
$user->locked = 0;
$locked = 0;
} else {
$user->locked = 1;
$locked = 1;
DB::table('sessions')
->where('user_id', $request['user_id'])
->delete();
}
$user->update(['locked' => $locked]);
}
I tried a number of ways from the Internet, but they are not able to achieve, I hope someone can give me a way of achieving ideas or methods, thank you for your help.
Let me provide an example. Define a SessionTimeout middleware in the app\Http\Middleware folder.
<?php
namespace App\Http\Middleware;
use Closure;
use Auth;
use Session;
class SessionTimeout
{
/**
* Check the incoming request for session data, log out if session lifetime is exceeded.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
//$isLoggedIn = $request->path() != '/logout';
$bag = Session::getMetadataBag();
$max = $this->getTimeOut();
if (($bag && $max < (time() - $bag->getLastUsed()))) {
//$cookie = cookie('intend', $isLoggedIn ? url()->current() : 'auth/login');
$email = Auth::user()->email;
$returnPath = url()->current();
$request->session()->flush(); // remove all the session data
Auth::logout(); // logout user
return redirect('auth/login')
->withInput(compact('email', 'returnPath'))
//->withCookie($cookie)
->withErrors(['Please login']);
//you could also redirect to lock-screen, a completely different view
//and then pass the returnPath to controller method maybe via hidden filed
//to redirect to the last page/path the user was on
//after successful re-login from the lock-screen.
}
return $next($request);
}
/**
* Set a variable in .env file TIMEOUT (in seconds) to play around in the development machine.
*/
protected function getTimeOut()
{
return (env('TIMEOUT')) ?: (config('session.lifetime') * 60);
}
}
The add SessionTimeout to the app\Http\Kernel.php
class Kernel extends HttpKernel {
/**
* The application's global HTTP middleware stack.
*
* #var array
*/
protected $middleware = [
'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
'Illuminate\Cookie\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'App\Http\Middleware\SessionTimeout'
];
/**
* The application's route middleware.
*
* #var array
*/
protected $routeMiddleware = [
'auth' => 'App\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated'
];
}
Then in the view for login form generally in resources\views\auth\login.blade.php
#extend('app-layout')
#section('content')
//code to display errors here
#if($email) //check if the request has $email returned by SessionTimeout middleware
//if so display lock screen like
//code to display the profile image
//code to display the user email (or whatever id is used)
#else
//display email input field for a new login
//code to input the email (whatever id is used) for a new login
#endif
//here the code common for lock screen as well as new login.
//code to display input password
//code for submit button and rest of the things like remember me field
#stop
You can also use partials for lock screen and new login form and display based on #if($email).
Hope this would get you started.
Assuming you are using the session driver to handle your authentication, you can change the time period for an idle session to expire in the
/app/config/session.php file.
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/
'lifetime' => 120, // minutes
'expire_on_close' => false,
I'm actually implementing 2-factor auth into the project. What I did was
Auth::user()->google2fa_passed = 1;
In fact it doesn't really store, when navigate to another page, the value is missing.
I also don't want to keep in another session, because when user logout (or users remove the session cookie from their browser), then will show a login page, and go through the 2 factor auth again.
Any idea how to save 1 more attribute to user session?
When you use Auth::user() it give you the Eloquent model of the authenticate user.
If you want to store data in session you need to use the Session facade or session() helper.
You can find more information about session in the documentation.
PS: Old version of the documentation is better (http://laravel.com/docs/5.0/session).
Eventually, I use session to store.
After key in the 6-digits code, store a flag into session
\Session::put('totp_passed', 1);
In app/Http/Middleware/Authenticate.php, remove the 2FA session if session expired
public function handle($request, Closure $next)
{
if ($this->auth->guest()) {
// remove the 2-factor auth if the user session expired
\Session::forget('totp_passed'); // <------- add this line
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
return redirect()->route('auth.login');
}
}
return $next($request);
}
Then create another middleware, e.g. app/Http/Middleware/TwoFactorAuth.php
namespace App\Http\Middleware;
use Closure;
class TwoFactorAuth
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (!\Session::has('totp_passed')) {
return redirect()->route('auth.2fa');
}
return $next($request);
}
}
In app/Http/Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'2fa' => \App\Http\Middleware\TwoFactorAuth::class, // <------ add this line
];
How to use
Route::group(['middleware' => 'auth'], function () {
// must be login first only can access this page
Route::get('2fa', ['as' => 'auth.2fa', 'uses' => 'Auth\AuthController#get2FactorAuthentication']);
Route::post('2fa', ['uses' => 'Auth\AuthController#post2FactorAuthentication']);
// add 2-factor auth middleware
Route::group(['middleware' => '2fa'], function () {
// all routes that required login
});
});