How does the Session table records delete itself? - laravel

I'm working with the Session table in Laravel and I just don't see how these records delete themselves after some time.
I haven't found any comprehensive guide on how the Session table works.
Here's my code so far:
SessionHandler.php
<?php
namespace App\Extensions;
class SessionHandler implements \SessionHandlerInterface
{
public function open($savePath, $sessionName) {}
public function close() {}
public function read($sessionId) {}
public function write($sessionId, $data) {}
public function destroy($sessionId) {}
public function gc($lifetime) {}
}
App\Session.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class Session extends Model
{
protected $hidden = ['payload'];
/**
* The database table used by the model.
*
* #var string
*/
public $table = 'sessions';
public $timestamps = false;
/**
* Returns the user that belongs to this entry.
*/
public function user()
{
return $this->belongsTo('User');
}
/**
* Returns all the guest users.
*
* #param $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeGuests($query)
{
return $query->whereNull('user_id')->where('last_activity', '>=', strtotime(Carbon::now()->subMinutes(25)));
}
/**
* Returns all the registered users.
*
* #param $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeRegistered($query)
{
return $query->whereNotNull('user_id')->where('last_activity', '>=', strtotime(Carbon::now()->subMinutes(25)))->with('user');
}
/**
* Updates the session of the current user.
*
* #param $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeUpdateCurrent($query)
{
return $query->where('id', Session::getId())->update([
'user_id' => ! empty(Auth::user()) ? Auth::id() : null
]);
}
}
AppServiceProvider.php
Session::extend('handler', function ($app) {
// Return implementation of SessionHandler
return new SessionHandler;
});
.env
SESSION_LIFETIME = 1
session.php
'lifetime' => env('SESSION_LIFETIME', 1),
'expire_on_close' => false,
What I'm basically trying to do is show each Guests/Users activity, but once their session/time limit is over -- POP! It deletes from the database too. Can anyone please share some insight on how to achieve this?

I think I just had to narrow down my search (or search better xD) --
You can set your session.php 'lifetime' value to however long you want the session to last -- and then for the 'lottery' value you can set it to 1,1 so that it fires every time a request is sent (I'm not sure of the performance effect of setting it to this) instead of the default 2,100. You probably should also change .env file SESSION_LIFETIME value to whenever you want it to expire (i.e. 1 for 1 minute).
So after 1 minute, on a request (via 'lottery' firing everytime), it deletes the session records with an expiry time of over one minute.

Related

Laravel 8 Fortify - 2FA only when the user logs in from a new device

I am implementing two-factor authentication (2FA) in my Laravel 8 application.
The 2FA is applied every time the user logs in. However, I don't really feel that 2FA is necessary every time, I even find it annoying. As a solution I am thinking of applying it only when the user connects from a new device. Is there someone who has already done it or who can give me a hint of the changes that would be necessary?
I have got it. Here are the steps I have followed:
In the config file fortify.php I have added
'pipelines' => [
'login' => [
App\Actions\Fortify\RedirectIfTwoFactorAuthenticatable::class,
Laravel\Fortify\Actions\AttemptToAuthenticate::class,
Laravel\Fortify\Actions\PrepareAuthenticatedSession::class,
]
]
I have added the field two_factor_cookies to the User class.
I have customized the RedirectIfTwoFactorAuthenticatable class of
Fortify:
<?php
namespace App\Actions\Fortify;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable as DefaultRedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;
class RedirectIfTwoFactorAuthenticatable extends DefaultRedirectIfTwoFactorAuthenticatable
{
/**
* Handle the incoming request.
*
* #param \Illuminate\Http\Request $request
* #param callable $next
* #return mixed
*/
public function handle($request, $next)
{
$user = $this->validateCredentials($request);
if (optional($user)->two_factor_secret &&
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user)) &&
$this->checkIfUserDeviceHasNotCookie($user)) {
return $this->twoFactorChallengeResponse($request, $user);
}
return $next($request);
}
/**
* This checks if the user's device has the cookie stored
* in the database.
*
* #param \App\Models\User\User $user
* #return bool
*/
protected function checkIfUserDeviceHasNotCookie($user)
{
$two_factor_cookies = json_decode($user->two_factor_cookies);
if (!is_array($two_factor_cookies)){
$two_factor_cookies = [];
}
$two_factor_cookie = \Cookie::get('2fa');
return !in_array($two_factor_cookie,$two_factor_cookies);
}
}
In the FortifyServiceProvider I have added a customized TwoFactorLoginResponse.
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Http\Responses\FailedPasswordResetLinkRequestResponse;
use App\Http\Responses\FailedPasswordResetResponse;
use App\Http\Responses\LockoutResponse;
use App\Http\Responses\LoginResponse;
use App\Http\Responses\LogoutResponse;
use App\Http\Responses\PasswordResetResponse;
use App\Http\Responses\RegisterResponse;
use App\Http\Responses\SuccessfulPasswordResetLinkRequestResponse;
use App\Http\Responses\TwoFactorLoginResponse;
use App\Http\Responses\VerifyEmail;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse as FailedPasswordResetLinkRequestResponseContract;
use Laravel\Fortify\Contracts\FailedPasswordResetResponse as FailedPasswordResetResponseContract;
use Laravel\Fortify\Contracts\LockoutResponse as LockoutResponseContract;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Laravel\Fortify\Contracts\LogoutResponse as LogoutResponseContract;
use Laravel\Fortify\Contracts\PasswordResetResponse as PasswordResetResponseContract;
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse as SuccessfulPasswordResetLinkRequestResponseContract;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->registerResponseBindings();
}
/**
* Register the response bindings.
*
* #return void
*/
protected function registerResponseBindings()
{
$this->app->singleton(LoginResponseContract::class, LoginResponse::class);
$this->app->singleton(LogoutResponseContract::class, LogoutResponse::class);
$this->app->singleton(TwoFactorLoginResponseContract::class, TwoFactorLoginResponse::class);
$this->app->singleton(RegisterResponseContract::class, RegisterResponse::class);
$this->app->singleton(LockoutResponseContract::class, LockoutResponse::class);
$this->app->singleton(SuccessfulPasswordResetLinkRequestResponseContract::class, SuccessfulPasswordResetLinkRequestResponse::class);
$this->app->singleton(FailedPasswordResetLinkRequestResponseContract::class, FailedPasswordResetLinkRequestResponse::class);
$this->app->singleton(PasswordResetResponseContract::class, PasswordResetResponse::class);
$this->app->singleton(FailedPasswordResetResponseContract::class, FailedPasswordResetResponse::class);
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Fortify::ignoreRoutes();
Fortify::loginView(function () {
return view('auth.login');
});
Fortify::twoFactorChallengeView('auth.two-factor-challenge');
Fortify::confirmPasswordView(function (Request $request) {
if ($request->ajax()) {
return view('auth.confirm-password-form');
} else {
return view('auth.confirm-password');
}
});
Fortify::requestPasswordResetLinkView(function () {
return view('auth.forgot-password');
});
Fortify::resetPasswordView(function ($request) {
return view('auth.reset-password', ['request' => $request,'token' => $request->route('token')]);
});
Fortify::registerView(function () {
return view('auth.register');
});
Fortify::verifyEmailView(function () {
return view('auth.verify');
});
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
/*RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->email.$request->ip());
});*/
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}
Finally, the TwoFactorLoginResponse:
<?php
namespace App\Http\Responses;
use Illuminate\Http\JsonResponse;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
class TwoFactorLoginResponse implements TwoFactorLoginResponseContract
{
/**
* Create an HTTP response that represents the object.
*
* #param \Illuminate\Http\Request $request
* #return \Symfony\Component\HttpFoundation\Response
*/
public function toResponse($request)
{
$user = \Auth::user();
$this->storeCookieIfNotInDB($user);
$role = $user->role;
if ($request->wantsJson()) {
return new JsonResponse('', 204);
}
if ($role == "0") {
return redirect()->route('user.home');
} else {
return redirect()->route('admin.home');
}
}
/**
* Store the cookie if it is not in the database.
*
* #param \App\Models\User\User $user
* #return void
*/
protected function storeCookieIfNotInDB($user)
{
$two_factor_cookies = json_decode($user->two_factor_cookies);
if (!is_array($two_factor_cookies)){
$two_factor_cookies = [];
}
$two_factor_cookie = \Cookie::get('2fa');
if (!in_array($two_factor_cookie,$two_factor_cookies)) {
$two_factor_cookie = md5(now());
$two_factor_cookies[] = $two_factor_cookie;
if (count($two_factor_cookies) > 3) {
array_shift($two_factor_cookies);
}
$user->two_factor_cookies = json_encode($two_factor_cookies);
$user->save();
$lifetime = 60 * 24 * 365; //one year
\Cookie::queue('2fa',$two_factor_cookie,$lifetime);
}
}
}
Upon login, it will look for the cookie 2fa. If its content is stored in the database, it will not be necessary to enter the code again. To prevent unlimited cookie content from being saved in the DB you can add a maximum limit (I have set it 3).
Thanks to Maarten Veerman for the inital help.
According to this line: https://github.com/laravel/fortify/blob/82c99b6999f7e89f402cfd7eb4074e619382b3b7/src/Http/Controllers/AuthenticatedSessionController.php#L80
you can create a pipelines.login entry in your fortify config file.
The solution would be to:
create the config entry
copy the pipeline setup in the above file, line 84.
create a custom AttemptToAuthenticate class, make sure the pipeline config points to your new class.
make the new class extend the default fortify AttemptToAuthenticate class.
overwrite the handle function, add your logic in the new function, where you check for a cookie on the device.

Laravel - Query scopes across models

In a nutshell, I want to create a function that my query scopes can use across multiple models:
public function scopeNormaliseCurrency($query,$targetCurrency) {
return $query->normaliseCurrencyFields(
['cost_per_day','cost_per_week'],
$targetCurrency
);
}
I have got my logic working within this scope function no problem, but I want to make this code available to all my models, as there are multiple currency fields in different tables and I don't want to be replicating the code in each query scope - only specify the columns that need attention.
So, where would I make my function normaliseCurrencyFields? I have extended the Model class as well as used the newCollection keyword to extend Collection but both result in Call to undefined method Illuminate\Database\Query\Builder::normaliseCurrencyFields() errors.
I have looked into Global Scoping but this seems to be localised to a Model.
Am I along the right lines? Should I be targeting Eloquent specifically?
Create an abstract base model that extends eloquent then extend it with the classes you want to have access to it. I do this for searching functions, uuid creation, and class code functions. So that all of my saved models are required to have to certain attributes and access to my searching functions. For instance I created a static search function getobjectbyid(). So that when extended I can call it like so:
$user = User::getobjectbyid('habwiifnbrklsnbbd1938');
Thus way I know I am getting a user object back.
My base model:
<?php
/**
* Created by PhpStorm.
* User: amac
* Date: 6/5/17
* Time: 12:45 AM
*/
namespace App;
use Illuminate\Database\Eloquent\Model as Eloquent;
abstract class Model extends Eloquent
{
protected $guarded = [
'class_code',
'id'
];
public $primaryKey = 'id';
public $incrementing = false;
public function __construct($attributes = array()) {
parent::__construct($attributes); // Eloquent
$this->class_code = \App\Enums\EnumClassCode::getValueByKey(get_class($this));
$this->id = $this->class_code . uniqid();
return $this;
}
public static function getObjectById($id){
$class = get_called_class();
$results = $class::find($id);
return $results;
}
public static function getAllObjects(){
$class = get_called_class();
return $class::all();
}
my user model:
<?php
namespace App;
use Mockery\Exception;
use Illuminate\Support\Facades\Hash;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use App\Model as Model;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'contact', 'username', 'email_address'
];
/**
* The column name of the "remember me" token.
*
* #var string
*/
protected $rememberTokenName = 'remember_token';
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'remember_token', 'active'
];
/**
* the attributes that should be guarded from Mass Assignment
*
* #var array
*/
protected $guarded = [
'created_at', 'updated_at', 'password_hash'
];
/**
* Define table to be used with this model. It defaults and assumes table names will have an s added to the end.
*for instance App\User table by default would be users
*/
protected $table = "user";
/**
* We have a non incrementing primary key
*
* #var bool
*/
public $incrementing = false;
/**
* relationships
*/
public function contact(){
// return $this->hasOne(Contact::class, 'id', 'contact_id');
return $this->hasOne(Contact::class);
}
public function customers(){
// return $this->hasOne(Contact::class, 'id', 'contact_id');
return $this->hasMany(Customer::class);
}
/**
* User constructor.
* #param array $attributes
*/
public function __construct($attributes = array()) {
parent::__construct($attributes); // Eloquent
// Your construct code.
$this->active = 1;
return $this;
}
/**
* #param $password string
* set user password_hash
* #return $this
*/
public function setPassword($password){
// TODO Password Validation
try{
$this->isActive();
$this->password_hash = Hash::make($password);
$this->save();
} catch(\Exception $e) {
dump($e->getMessage());
}
return $this;
}
/**
* Returns whether or not this use is active.
*
* #return bool
*/
public function isActive(){
if($this->active) {
return true;
} else {
Throw new Exception('This user is not active. Therefore you cannot change the password', 409);
}
}
public function getEmailUsername(){
$contact = Contact::getObjectById($this->contact_id);
$email = Email::getObjectById($contact->email_id);
return $email->username_prefix;
}
/**
* #return string
*
* getFullName
* returns concatenated first and last name of user.
*/
public function getFullName(){
return $this->first_name . ' ' . $this->last_name;
}
/**
* Get the name of the unique identifier for the user.
*
* #return string
*/
public function getAuthIdentifierName(){
return $this->getKeyName();
}
/**
* Get the unique identifier for the user.
*
* #return mixed
*/
public function getAuthIdentifier(){
return $this->{$this->getAuthIdentifierName()};
}
/**
* Get the password for the user.
*
* #return string
*/
public function getAuthPassword(){
return $this->password_hash;
}
/**
* Get the token value for the "remember me" session.
*
* #return string
*/
public function getRememberToken(){
if (! empty($this->getRememberTokenName())) {
return $this->{$this->getRememberTokenName()};
}
}
/**
* Set the token value for the "remember me" session.
*
* #param string $value
* #return void
*/
public function setRememberToken($value){
if (! empty($this->getRememberTokenName())) {
$this->{$this->getRememberTokenName()} = $value;
}
}
/**
* Get the column name for the "remember me" token.
*
* #return string
*/
public function getRememberTokenName(){
return $this->rememberTokenName;
}
/**
* Get the e-mail address where password reset links are sent.
*
* #return string
*/
public function getEmailForPasswordReset(){
}
/**
* Send the password reset notification.
*
* #param string $token
* #return void
*/
public function sendPasswordResetNotification($token){
}
public function validateAddress(){
}
}
a TestController:
public function test(){
$user = User::getObjectById('USR594079ca59746');
$customers = array();
foreach ($user->customers as $customer){
$contact = Contact::getObjectById($customer->contact_id);
$name = PersonName::getObjectById($contact->personname_id);
$c = new \stdClass();
$c->id = $customer->id;
$c->name = $name->preferred_name;
$customers[] = $c;
}
$response = response()->json($customers);
return $response;
}
Take note on how getObjectById is extended and available to my other classes that extend my base model. Also I do not have to specify in my user model an 'id' or 'class_code' and when my user model is constructed it calls the parent constructor which is the constructor on my base model that handles 'id' and 'class_code'.

How to disable laravel 5.2 password bcrypt

I want to disable the laravel password bcrypt when I try to log-in like this
Auth::guard('client')->attempt(
'id' => $request['id'],
'password' => $request['password'])
But it seems to be more dificult than I thought, I know I should not do this but I temporally need to work like this, but laravel forces me to use encrypted passwords. I need to be able to use plain passwords on my database.
I been searching on internet but I cant find a solution.
Try extending SessionGuard and overriding function hasValidCredentials()
Create A file by name 'SessionGuardExtended' in App\CoreExtensions
use Illuminate\Auth\SessionGuard;
use Illuminate\Contracts\Auth\Authenticatable;
class SessionGuardExtended extends SessionGuard
{
/**
* Determine if the user matches the credentials.
*
* #param mixed $user
* #param array $credentials
* #return bool
*/
protected function hasValidCredentials($user, $credentials)
{
return ! is_null($user) && $credentials['password'] == $user->getAuthPassword();
}
}
Edit config/auth.php edit the driver and use sessionExtended
'web' => [
'driver' => 'sessionExtended',
'provider' => 'users',
],
In AppServiceProvider Write Code in boot function
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Auth::extend(
'sessionExtended',
function ($app) {
$provider = new EloquentUserProvider($app['hash'], config('auth.providers.users.model'));
return new SessionGuardExtended('sessionExtended', $provider, app()->make('session.store'), request());
}
);
}
Reference: Extending Laravel 5.2 SessionGuard
You can extend Illuminate\Auth\EloquentUserProvider, ie:
<?php
namespace App\Services\Auth;
use Illuminate\Auth\EloquentUserProvider as BaseUserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
class UserProvider extends BaseUserProvider {
/**
* Create a new database user provider.
*
* #param string $model
*
* #return void
*/
public function __construct($model)
{
$this->model = $model;
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
*
* #return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
// $matches = some method of matching $plain with $user->getAuthPassword();
return $matches;
}
}
Then register this in the IoC in a service provider like so:
<?php // ...
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
// ...
$this->app['auth']->extend(
'legacy',
function () {
return new \Illuminate\Auth\Guard(
new \App\Services\Auth\UserProvider(
$this->app['config']['auth.model']
),
$this->app['session.store']
);
}
);
// ...
}
Then set your current driver to legacy in config/auth.php.
PS: You may want to include the classes in the provider,
You can use
Auth::guard('client')->login($user);
In this $user is an instance of Model which is implemented Illuminate\Contracts\Auth\Authenticatable contract.
In user model it is already implemented.
Maybe it will helpful

Laravel Resource Router ignoring eager loaded data and getting a new query

In my resource router, I have this function, which is supposed to eager load only the rows that have the configureName of $id. The problem is that when I call $demo->demoSettings resource router is ignoring my eager load and grabbing all of the demoSettings related to this $demo.
This isn't the only instance of this problem that I have. I also have a couple loops where I am injecting data into my api index from related tables for convenience purposes. The moment I add a call to access my eager loaded data, it queries again and injects the data into my resource return. I've worked around it by using unset in the other instances, but this one is different because I actually want the list of data, rather than a single value.
public function show($demoId,$id)
{
$demo = Demo::with(array('DemoSettings' => function($query) use ($id)
{
$query->where('configureName', '=', $id);
}))->where('demoId','=',$demoId)->first();
return $demo->demoSettings;
}
Demo Model
class Demo extends Eloquent {
protected $table = 'Demo';
public $timestamps = false;
/**
* The key column used by the model.
* #var string
*/
protected $primaryKey = 'idDemos';
/**
* one(demo) to many(demoSettings) relationship
* #return array of the demoSettings associated with the demo
*/
public function demoSettings()
{
return $this->hasMany('DemoSettings','idDemos','idDemos')->orderBy('configureName','asc');
}
}
DemoSettings Model
class DemoSettings extends Eloquent {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'DemoSettings';
/**
* The key column used by the model.
* #var string
*/
protected $primaryKey = 'idDemoSettings';
/**
* many(demoSettings) to one(demo) relationship
* #return demo the parent record
*/
public function demo()
{
return $this->belongsTo('Demo','idDemos','idDemos');
}
}
Route
Route::group(array('prefix' => 'api/v1'), function(){
Route::group(array('prefix' => 'demos'), function(){
Route::resource('/', 'api_DemosController', array('only' => array('index','store','destroy','show')));
Route::resource('/{demoId}/settings', 'api_DemoSettingsController', array('only' => array('index','store','show')));
});
});

Logging in a user?

I'm having trouble with logging users in, everything appears to be in the right place, I get no errors in the log, but users fail to log in, I am using the correct credentials that are in my database.
Please note I have a different set up to the normal one:
My table is called test_users
My model sits in a separate namespace called Test
Here's my code:
In config>auth I have set:
'model' => '\Test\User',
'table' => 'test_users',
Here is how I call the Auth:
public function logIn()
{
$input = Input::all();
$credentials = array('email' => $input['email'], 'password' => $input['password']);
$input['remember-me'] = isset($input['remember-me']) ? true : false;
if(Auth::attempt($credentials, $input['remember-me']))
{
$this->output['message'] = 'ok';
}
else
{
$this->output['message'] = 'fail';
}
return $this->output;
}
Here's my model:
<?php namespace Test;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;
use Eloquent;
class User extends Eloquent implements UserInterface, RemindableInterface {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'test_users';
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = array('password');
/**
* Get the unique identifier for the user.
*
* #return mixed
*/
public function getAuthIdentifier()
{
return $this->getKey();
}
/**
* Get the password for the user.
*
* #return string
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* Get the e-mail address where password reminders are sent.
*
* #return string
*/
public function getReminderEmail()
{
return $this->email;
}
}
Auth::attempt checks for a hashed password. It appears you might be trying to set them with plaintext. Try setting your passwords with Hash::make('password') if you aren't already.

Resources