Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling browser sessions in redis #1553

Conversation

dmitritarasov
Copy link

@dmitritarasov dmitritarasov commented Dec 11, 2024

This update allows managing browser sessions even when using Redis for session storage. It doesn't change existing functionality but adds new capabilities.

Moreover, it works correctly with the remember_me flag by ending all sessions on other devices except the current one.

The principle is simple: sessions are stored as usual, but we also save session IDs and other parameters in Redis for every user. This allows us to quickly find and end all active user sessions if needed.

The benefit of this update is confirmed by numerous requests for this improvement
#350
#1270

@taylorotwell
Copy link
Member

Hi there! I don't want to pour much more code into Jetstream because we are just now kicking off our starter kit rebuild projects internally. 👍

@rivalex
Copy link

rivalex commented Dec 18, 2024

Hello everyone ...
I have the same problem and I solved it by extending the LogoutOtherBrowserSessionsForm class.
In my project it works perfectly, I hope it can be of help or inspiration for improvements.

<?php

namespace App\Livewire\Backend\User\UserEdit\Profile;

use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redis;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Agent;
use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm;
use Livewire\Attributes\On;

class UserSessions extends LogoutOtherBrowserSessionsForm
{
	const PREFIX = 'my_prefix_';

	/**
	 * Get the user's sessions stored in Redis.
	 *
	 * @return array The user's sessions stored in Redis.
	 */
	public function getRedisUserSessions(): array
	{
		$keys = array_map(fn($key) => str_replace(self::PREFIX, '', $key), Redis::keys('my_key_*'));
		$data = array_map(fn($data) => unserialize(unserialize($data)), Redis::mget($keys));

		$redisData = [];
		foreach ($data as $item) {
			if (isset($item['user']) && $item['user'] === Auth::user()->id) {
				$redisData[] = $item;
			}
		}

		return $redisData;
	}

	/**
	 * Delete the other browser session records from storage.
	 *
	 * @return void
	 */
	protected function deleteOtherSessionRecords(): void
	{
		$activeSessions = [];
		$keys = array_map(fn($key) => str_replace(self::PREFIX, '', $key), Redis::keys('my_key_*'));
		foreach ($keys as $key) {
			$redis = Redis::mget([$key]);
			$activeSessions[] = array_merge(unserialize(unserialize($redis[0])), ['key' => $key]);
		}

		if (count($activeSessions) === 0) {
			return;
		}

		foreach ($activeSessions as $activeSession) {
			if ($activeSession['id'] !== request()->session()->getId()) {
				Redis::del($activeSession['key']);
			}
		}
	}

	/**
	 * Log out from other browser sessions.
	 *
	 * @param StatefulGuard $guard
	 *
	 * @return void
	 * @throws AuthenticationException
	 */
	public function logoutOtherBrowserSessions(StatefulGuard $guard): void
	{
		$this->resetErrorBag();

		if (!Hash::check($this->password, Auth::user()->password)) {
			throw ValidationException::withMessages([
				'password' => [__('This password does not match our records.')],
			]);
		}

		$guard->logoutOtherDevices($this->password);

		$this->deleteOtherSessionRecords();

		request()->session()->put([
			'password_hash_' . Auth::getDefaultDriver() => Auth::user()->getAuthPassword(),
		]);

		$this->confirmingLogout = false;

		$this->dispatch('loggedOut');
	}

	/**
	 * Get the current sessions.
	 *
	 * @return Collection
	 */
	#[On('loggedOut')]
	public function getSessionsProperty(): Collection
	{
		return collect($this->getRedisUserSessions()
		)->map(function ($session) {
			return (object)[
				'agent' => $this->createAgent($session),
				'ip_address' => request()->ip(),
				'is_current_device' => $session['id'] === request()->session()->getId(),
				'last_active' => Carbon::createFromTimestamp($session['last_activity'])->diffForHumans(),
			];
		});
	}

	/**
	 * Create a new agent instance from the given session.
	 *
	 * @param mixed $session
	 *
	 * @return Agent
	 */
	protected function createAgent($session): Agent
	{
		return tap(new Agent(), fn($agent) => $agent->setUserAgent($session['agent']));
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants