Force logout on routes not watched by the KickOutInactiveUser middleware

This commit is contained in:
Bubka 2024-10-26 18:45:30 +02:00
parent 3ad068d1d7
commit 528a3be458
7 changed files with 60 additions and 35 deletions

View File

@ -73,12 +73,14 @@ class Kernel extends HttpKernel
protected $middlewareAliases = [ protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'admin' => \App\Http\Middleware\AdminOnly::class, 'admin' => \App\Http\Middleware\AdminOnly::class,
'guest' => \App\Http\Middleware\RejectIfAuthenticated::class, 'rejectIfAuthenticated' => \App\Http\Middleware\RejectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'rejectIfDemoMode' => \App\Http\Middleware\RejectIfDemoMode::class, 'rejectIfDemoMode' => \App\Http\Middleware\RejectIfDemoMode::class,
'rejectIfReverseProxy' => \App\Http\Middleware\RejectIfReverseProxy::class, 'rejectIfReverseProxy' => \App\Http\Middleware\RejectIfReverseProxy::class,
'RejectIfSsoOnlyAndNotForAdmin' => \App\Http\Middleware\RejectIfSsoOnlyAndNotForAdmin::class, 'RejectIfSsoOnlyAndNotForAdmin' => \App\Http\Middleware\RejectIfSsoOnlyAndNotForAdmin::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'kickOutInactiveUser' => \App\Http\Middleware\KickOutInactiveUser::class,
'forceLogout' => \App\Http\Middleware\ForceLogout::class,
// 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, // 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
// 'signed' => \App\Http\Middleware\ValidateSignature::class, // 'signed' => \App\Http\Middleware\ValidateSignature::class,
]; ];

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class ForceLogout
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string $guards
* @return mixed
*/
public function handle($request, Closure $next, ...$guards)
{
if (Auth::user() != null) {
Auth::guard('web-guard')->logoutCurrentDevice();
}
return $next($request);
}
}

View File

@ -43,27 +43,30 @@ class LogoutListener extends AbstractAccessListener
* @var \App\Models\User * @var \App\Models\User
*/ */
$user = $event->user; $user = $event->user;
$ip = config('2fauth.proxy_headers.forIp')
? $this->request->header(config('2fauth.proxy_headers.forIp'), $this->request->ip())
: $this->request->ip();
$userAgent = $this->request->userAgent();
$log = $user->authentications()
->whereIpAddress($ip)
->whereUserAgent($userAgent)
->whereGuard($event->guard)
->orderByDesc('login_at')
->first();
if (! $log) { if ($user != null) {
$log = new AuthLog([ $ip = config('2fauth.proxy_headers.forIp')
'ip_address' => $ip, ? $this->request->header(config('2fauth.proxy_headers.forIp'), $this->request->ip())
'user_agent' => $userAgent, : $this->request->ip();
'guard' => $event->guard, $userAgent = $this->request->userAgent();
'login_method' => $this->loginMethod(), $log = $user->authentications()
]); ->whereIpAddress($ip)
->whereUserAgent($userAgent)
->whereGuard($event->guard)
->orderByDesc('login_at')
->first();
if (! $log) {
$log = new AuthLog([
'ip_address' => $ip,
'user_agent' => $userAgent,
'guard' => $event->guard,
'login_method' => $this->loginMethod(),
]);
}
$log->logout_at = now();
$user->authentications()->save($log);
} }
$log->logout_at = now();
$user->authentications()->save($log);
} }
} }

View File

@ -34,8 +34,9 @@ use Laravel\Passport\Http\Controllers\PersonalAccessTokenController;
/** /**
* Routes that only work for unauthenticated user (return an error otherwise) * Routes that only work for unauthenticated user (return an error otherwise)
* 'kickOutInactiveUser',
*/ */
Route::group(['middleware' => ['guest', 'rejectIfDemoMode', 'RejectIfSsoOnlyAndNotForAdmin']], function () { Route::group(['middleware' => ['rejectIfDemoMode', 'RejectIfSsoOnlyAndNotForAdmin', 'forceLogout']], function () {
Route::post('user', [RegisterController::class, 'register'])->name('user.register'); Route::post('user', [RegisterController::class, 'register'])->name('user.register');
Route::post('user/password/lost', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('user.password.lost'); Route::post('user/password/lost', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('user.password.lost');
Route::post('user/password/reset', [ResetPasswordController::class, 'reset'])->name('password.reset'); Route::post('user/password/reset', [ResetPasswordController::class, 'reset'])->name('password.reset');
@ -46,15 +47,15 @@ Route::group(['middleware' => ['guest', 'rejectIfDemoMode', 'RejectIfSsoOnlyAndN
/** /**
* Routes that can be requested max 10 times per minute by the same IP * Routes that can be requested max 10 times per minute by the same IP
*/ */
Route::group(['middleware' => ['rejectIfDemoMode', 'throttle:10,1', 'RejectIfSsoOnlyAndNotForAdmin']], function () { Route::group(['middleware' => ['rejectIfDemoMode', 'throttle:10,1', 'RejectIfSsoOnlyAndNotForAdmin', 'forceLogout']], function () {
Route::post('webauthn/recover', [WebAuthnRecoveryController::class, 'recover'])->name('webauthn.recover'); Route::post('webauthn/recover', [WebAuthnRecoveryController::class, 'recover'])->name('webauthn.recover');
}); });
/** /**
* Routes that only work for unauthenticated user (return an error otherwise) * Routes that only work for unauthenticated user (return an error otherwise)
* that can be requested max 10 times per minute by the same IP * that can be requested max 10 times per minute by the same IP 'kickOutInactiveUser',
*/ */
Route::group(['middleware' => ['guest', 'throttle:10,1']], function () { Route::group(['middleware' => ['forceLogout', 'throttle:10,1']], function () {
Route::post('user/login', [LoginController::class, 'login'])->name('user.login')->middleware('RejectIfSsoOnlyAndNotForAdmin'); Route::post('user/login', [LoginController::class, 'login'])->name('user.login')->middleware('RejectIfSsoOnlyAndNotForAdmin');
Route::post('webauthn/login', [WebAuthnLoginController::class, 'login'])->name('webauthn.login')->middleware('RejectIfSsoOnlyAndNotForAdmin'); Route::post('webauthn/login', [WebAuthnLoginController::class, 'login'])->name('webauthn.login')->middleware('RejectIfSsoOnlyAndNotForAdmin');

View File

@ -95,7 +95,7 @@ class ForgotPasswordControllerTest extends FeatureTestCase
} }
#[Test] #[Test]
public function test_submit_email_password_request_when_authenticated_returns_bad_request() public function test_submit_email_password_request_when_authenticated_returns_ok()
{ {
/** /**
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
@ -106,7 +106,7 @@ class ForgotPasswordControllerTest extends FeatureTestCase
->json('POST', '/user/password/lost', [ ->json('POST', '/user/password/lost', [
'email' => $user->email, 'email' => $user->email,
]) ])
->assertStatus(400) ->assertStatus(200)
->assertJsonStructure([ ->assertJsonStructure([
'message', 'message',
]); ]);

View File

@ -250,10 +250,7 @@ class LoginTest extends FeatureTestCase
'email' => $this->user->email, 'email' => $this->user->email,
'password' => self::PASSWORD, 'password' => self::PASSWORD,
]) ])
->assertStatus(400) ->assertStatus(200);
->assertJsonStructure([
'message',
]);
} }
#[Test] #[Test]

View File

@ -275,10 +275,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
->assertOk(); ->assertOk();
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE) $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
->assertStatus(400) ->assertStatus(200);;
->assertJsonStructure([
'message',
]);
} }
#[Test] #[Test]