Skip to content

Commit 42e2419

Browse files
committed
User: added $persistIdentity option to control identity availability after logout
By default the identity stays available after logout or expiration (for personalization). Setting $persistIdentity to false discards it, so getIdentity() and getId() return null when not logged in. Configurable via the security.authentication DI section.
1 parent 5861d18 commit 42e2419

5 files changed

Lines changed: 123 additions & 7 deletions

File tree

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ Importantly, **when user logs out, identity is not deleted** and is still availa
178178

179179
Thanks to this, you can still assume which user is at the computer and, for example, display personalized offers in the e-shop, however, you can only display his personal data after logging in.
180180

181+
If you prefer the identity to be discarded on every logout and expiration, set `$user->persistIdentity = false`. Retaining the identity is best-effort and depends on the storage implementation.
182+
181183
Identity is an object that implements the [Nette\Security\IIdentity](https://api.nette.org/master/Nette/Security/IIdentity.html) interface, the default implementation is [Nette\Security\SimpleIdentity](https://api.nette.org/3.0/Nette/Security/SimpleIdentity.html). And as mentioned, identity is stored in the session, so if, for example, we change the role of some of the logged-in users, old data will be kept in the identity until he logs in again.
182184

183185

src/Bridges/SecurityDI/SecurityExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* authentication: object{
2525
* storage: 'session'|'cookie',
2626
* expiration: string|null,
27+
* persistIdentity: bool,
2728
* cookieName: string|null,
2829
* cookieDomain: string|null,
2930
* cookieSamesite: 'Lax'|'Strict'|'None'|null,
@@ -57,6 +58,7 @@ public function getConfigSchema(): Nette\Schema\Schema
5758
'authentication' => Expect::structure([
5859
'storage' => Expect::anyOf('session', 'cookie')->default('session'),
5960
'expiration' => Expect::string()->dynamic(),
61+
'persistIdentity' => Expect::bool(true),
6062
'cookieName' => Expect::string(),
6163
'cookieDomain' => Expect::string(),
6264
'cookieSamesite' => Expect::anyOf('Lax', 'Strict', 'None'),
@@ -96,6 +98,10 @@ public function loadConfiguration(): void
9698
$user->addSetup('setExpiration', [$auth->expiration]);
9799
}
98100

101+
if (!$auth->persistIdentity) {
102+
$user->addSetup('$persistIdentity', [false]);
103+
}
104+
99105
if ($config->users) {
100106
$usersList = $usersRoles = $usersData = [];
101107
foreach ($config->users as $username => $data) {

src/Security/User.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class User
5050
/** default role for authenticated user without own identity */
5151
public string $authenticatedRole = 'authenticated';
5252

53+
/** keep identity available (via getIdentity() and getId()) after logout or expiration; depends on the storage implementation */
54+
public bool $persistIdentity = true;
55+
5356
/** @var array<callable(static): void> Occurs when the user is successfully logged in */
5457
public array $onLoggedIn = [];
5558

@@ -112,10 +115,11 @@ public function login(
112115

113116
/**
114117
* Logs out the user from the current session. The identity is kept available afterwards,
115-
* unless $clearIdentity is set.
118+
* unless $clearIdentity is set or the $persistIdentity property is disabled.
116119
*/
117120
final public function logout(bool $clearIdentity = false): void
118121
{
122+
$clearIdentity = $clearIdentity || !$this->persistIdentity;
119123
$logged = $this->isLoggedIn();
120124
$this->storage->clearAuthentication($clearIdentity);
121125
$this->authenticated = false;
@@ -139,8 +143,8 @@ final public function isLoggedIn(): bool
139143

140144

141145
/**
142-
* Returns the user identity. It may be available even when not logged in (e.g. after logout or expiration),
143-
* so its presence does not imply the user is logged in; null if none.
146+
* Returns the user identity. It may be available even when not logged in (e.g. after logout or expiration)
147+
* unless $persistIdentity is disabled, so its presence does not imply the user is logged in; null if none.
144148
*/
145149
final public function getIdentity(): ?IIdentity
146150
{
@@ -161,10 +165,11 @@ private function loadStoredData(): void
161165
$this->logoutReason = $reason;
162166
})(...$this->storage->getState());
163167

164-
$this->identity = $identity && $this->authenticator instanceof IdentityHandler
168+
$identity = $identity && $this->authenticator instanceof IdentityHandler
165169
? $this->authenticator->wakeupIdentity($identity)
166170
: $identity;
167-
$this->authenticated = $this->authenticated && $this->identity !== null;
171+
$this->authenticated = $this->authenticated && $identity !== null;
172+
$this->identity = !$this->authenticated && !$this->persistIdentity ? null : $identity;
168173
}
169174

170175

@@ -229,11 +234,11 @@ final public function hasAuthenticator(): bool
229234

230235
/**
231236
* Enables log out after inactivity (like '20 minutes'). The identity is kept available afterwards,
232-
* unless $clearIdentity is set.
237+
* unless $clearIdentity is set or the $persistIdentity property is disabled.
233238
*/
234239
public function setExpiration(?string $expire, bool $clearIdentity = false): static
235240
{
236-
$this->storage->setExpiration($expire, $clearIdentity);
241+
$this->storage->setExpiration($expire, $clearIdentity || !$this->persistIdentity);
237242
return $this;
238243
}
239244

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* Test: SecurityExtension persistIdentity
5+
*/
6+
7+
use Nette\Bridges\HttpDI\HttpExtension;
8+
use Nette\Bridges\HttpDI\SessionExtension;
9+
use Nette\Bridges\SecurityDI\SecurityExtension;
10+
use Nette\DI;
11+
use Tester\Assert;
12+
13+
14+
require __DIR__ . '/../bootstrap.php';
15+
16+
17+
test('defaults to true', function () {
18+
$compiler = new DI\Compiler;
19+
$compiler->addExtension('foo', new HttpExtension);
20+
$compiler->addExtension('bar', new SessionExtension);
21+
$compiler->addExtension('security', new SecurityExtension);
22+
$compiler->setClassName('ContainerDefault');
23+
24+
eval($compiler->compile());
25+
$container = new ContainerDefault;
26+
27+
Assert::true($container->getService('security.user')->persistIdentity);
28+
});
29+
30+
31+
test('disabled via configuration', function () {
32+
$compiler = new DI\Compiler;
33+
$compiler->addExtension('foo', new HttpExtension);
34+
$compiler->addExtension('bar', new SessionExtension);
35+
$compiler->addExtension('security', new SecurityExtension);
36+
$compiler->setClassName('ContainerDisabled');
37+
38+
$loader = new Nette\DI\Config\Loader;
39+
$config = $loader->load(Tester\FileMock::create('
40+
security:
41+
authentication:
42+
persistIdentity: false
43+
', 'neon'));
44+
45+
eval($compiler->addConfig($config)->compile());
46+
$container = new ContainerDisabled;
47+
48+
Assert::false($container->getService('security.user')->persistIdentity);
49+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* Test: Nette\Security\User persistIdentity.
5+
*/
6+
7+
use Nette\Security\SimpleIdentity;
8+
use Nette\Security\User;
9+
use Tester\Assert;
10+
11+
12+
require __DIR__ . '/../bootstrap.php';
13+
require __DIR__ . '/MockUserStorage.php';
14+
15+
16+
test('identity is kept after logout by default', function () {
17+
$user = new User(new MockUserStorage);
18+
$user->login(new SimpleIdentity('John Doe', 'admin'));
19+
20+
$user->logout();
21+
22+
Assert::false($user->isLoggedIn());
23+
Assert::equal(new SimpleIdentity('John Doe', 'admin'), $user->getIdentity());
24+
Assert::same('John Doe', $user->getId());
25+
});
26+
27+
28+
test('identity is discarded after logout when persistIdentity is off', function () {
29+
$user = new User(new MockUserStorage);
30+
$user->persistIdentity = false;
31+
$user->login(new SimpleIdentity('John Doe', 'admin'));
32+
33+
$user->logout();
34+
35+
Assert::false($user->isLoggedIn());
36+
Assert::null($user->getIdentity());
37+
Assert::null($user->getId());
38+
});
39+
40+
41+
test('identity already stored without authentication is not exposed when persistIdentity is off', function () {
42+
$storage = new MockUserStorage;
43+
$user = new User($storage);
44+
$user->login(new SimpleIdentity('John Doe', 'admin'));
45+
$user->logout(); // identity stays in storage, not authenticated
46+
47+
// fresh User over the same storage with persistIdentity off (e.g. after disabling it in config)
48+
$user = new User($storage);
49+
$user->persistIdentity = false;
50+
51+
Assert::false($user->isLoggedIn());
52+
Assert::null($user->getIdentity());
53+
Assert::null($user->getId());
54+
});

0 commit comments

Comments
 (0)