Skip to content

Commit d3639d8

Browse files
feature #59682 [Security] Deprecate UserInterface & TokenInterface's eraseCredentials() (chalasr, nicolas-grekas)
This PR was merged into the 7.3 branch. Discussion ---------- [Security] Deprecate UserInterface & TokenInterface's `eraseCredentials()` | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | yes | Issues | Fix #57842 | License | MIT As promised, this PR adds a commit on top of #59106 to improve the BC layer. This approach didn't fit in a review comment :) /cc `@chalasr` This PR leverages the new `#[\Deprecated]` attribute to decide if some `eraseCredentials()` method is to be called or not. My target DX here is to save us all (the community) from having to add `erase_credentials: false` configuration in all our apps. So, instead of having to opt-out from the deprecation using this config option, the opt-out is done by adding the attribute on the method: Before: ```php public function eraseCredentials(): void { } ``` After: ```php #[\Deprecated] public function eraseCredentials(): void { } // If your eraseCredentials() method was used to empty a "password" property: public function __serialize(): array { $data = (array) $this; unset($data["\0".self::class."\0password"]); return $data; } ``` This should provide a smoother upgrade path (and maker-bundle could be updated right-away). Commits ------- e5c94e62283 [Security] Improve BC-layer to deprecate eraseCredentials methods e5566065783 [Security] Deprecate `UserInterface` & `TokenInterface`'s `eraseCredentials()`
2 parents f3c03a2 + 8b0dda3 commit d3639d8

File tree

6 files changed

+67
-29
lines changed

6 files changed

+67
-29
lines changed

Authentication/AuthenticatorManager.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Psr\Log\LoggerInterface;
1515
use Symfony\Component\HttpFoundation\Request;
1616
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
1718
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1819
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1920
use Symfony\Component\Security\Core\AuthenticationEvents;
@@ -208,8 +209,8 @@ private function executeAuthenticator(AuthenticatorInterface $authenticator, Req
208209
// announce the authentication token
209210
$authenticatedToken = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($authenticatedToken, $passport))->getAuthenticatedToken();
210211

211-
if (true === $this->eraseCredentials) {
212-
$authenticatedToken->eraseCredentials();
212+
if ($this->eraseCredentials) {
213+
self::checkEraseCredentials($authenticatedToken)?->eraseCredentials();
213214
}
214215

215216
$this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS);
@@ -287,4 +288,41 @@ private function isSensitiveException(AuthenticationException $exception): bool
287288

288289
return false;
289290
}
291+
292+
/**
293+
* @deprecated since Symfony 7.3
294+
*/
295+
private static function checkEraseCredentials(TokenInterface|UserInterface|null $token): TokenInterface|UserInterface|null
296+
{
297+
if (!$token || !method_exists($token, 'eraseCredentials')) {
298+
return null;
299+
}
300+
301+
static $genericImplementations = [];
302+
$m = null;
303+
304+
if (!isset($genericImplementations[$token::class])) {
305+
$m = new \ReflectionMethod($token, 'eraseCredentials');
306+
$genericImplementations[$token::class] = AbstractToken::class === $m->class;
307+
}
308+
309+
if ($genericImplementations[$token::class]) {
310+
return self::checkEraseCredentials($token->getUser());
311+
}
312+
313+
static $deprecatedImplementations = [];
314+
315+
if (!isset($deprecatedImplementations[$token::class])) {
316+
$m ??= new \ReflectionMethod($token, 'eraseCredentials');
317+
$deprecatedImplementations[$token::class] = !$m->getAttributes(\Deprecated::class);
318+
}
319+
320+
if ($deprecatedImplementations[$token::class]) {
321+
trigger_deprecation('symfony/security-http', '7.3', 'Implementing "%s::eraseCredentials()" is deprecated since Symfony 7.3; add the #[\Deprecated] attribute on the method to signal its either empty or that you moved the logic elsewhere, typically to the "__serialize()" method.', get_debug_type($token));
322+
323+
return $token;
324+
}
325+
326+
return null;
327+
}
290328
}

Tests/Authentication/AuthenticatorManagerTest.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
use PHPUnit\Framework\TestCase;
1616
use Psr\Log\AbstractLogger;
1717
use Psr\Log\LoggerInterface;
18+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
1819
use Symfony\Component\EventDispatcher\EventDispatcher;
1920
use Symfony\Component\HttpFoundation\Request;
2021
use Symfony\Component\HttpFoundation\Response;
22+
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
2123
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
2224
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
2325
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
@@ -40,6 +42,8 @@
4042

4143
class AuthenticatorManagerTest extends TestCase
4244
{
45+
use ExpectDeprecationTrait;
46+
4347
private MockObject&TokenStorageInterface $tokenStorage;
4448
private EventDispatcher $eventDispatcher;
4549
private Request $request;
@@ -184,6 +188,8 @@ public function testAllRequiredBadgesPresent()
184188
}
185189

186190
/**
191+
* @group legacy
192+
*
187193
* @dataProvider provideEraseCredentialsData
188194
*/
189195
public function testEraseCredentials($eraseCredentials)
@@ -193,12 +199,25 @@ public function testEraseCredentials($eraseCredentials)
193199

194200
$authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter', fn () => $this->user)));
195201

196-
$authenticator->expects($this->any())->method('createToken')->willReturn($this->token);
202+
$token = new class extends AbstractToken {
203+
public $erased = false;
204+
205+
public function eraseCredentials(): void
206+
{
207+
$this->erased = true;
208+
}
209+
};
197210

198-
$this->token->expects($eraseCredentials ? $this->once() : $this->never())->method('eraseCredentials');
211+
$authenticator->expects($this->any())->method('createToken')->willReturn($token);
212+
213+
if ($eraseCredentials) {
214+
$this->expectDeprecation(\sprintf('Since symfony/security-http 7.3: Implementing "%s@anonymous::eraseCredentials()" is deprecated since Symfony 7.3; add the #[\Deprecated] attribute on the method to signal its either empty or that you moved the logic elsewhere, typically to the "__serialize()" method.', AbstractToken::class));
215+
}
199216

200217
$manager = $this->createManager([$authenticator], 'main', $eraseCredentials, exposeSecurityErrors: ExposeSecurityLevel::None);
201218
$manager->authenticateRequest($this->request);
219+
220+
$this->assertSame($eraseCredentials, $token->erased);
202221
}
203222

204223
public static function provideEraseCredentialsData()
@@ -403,7 +422,7 @@ public function log($level, $message, array $context = []): void
403422
}
404423
};
405424

406-
$manager = $this->createManager([$authenticator], 'main', true, [], $logger, exposeSecurityErrors: ExposeSecurityLevel::None);
425+
$manager = $this->createManager([$authenticator], 'main', false, [], $logger, exposeSecurityErrors: ExposeSecurityLevel::None);
407426
$response = $manager->authenticateRequest($this->request);
408427
$this->assertSame($this->response, $response);
409428
$this->assertStringContainsString($authenticator::class, $logger->logContexts[0]['authenticator']);
@@ -422,7 +441,7 @@ private static function createDummySupportsAuthenticator(?bool $supports = true)
422441
return new DummySupportsAuthenticator($supports);
423442
}
424443

425-
private function createManager($authenticators, $firewallName = 'main', $eraseCredentials = true, array $requiredBadges = [], ?LoggerInterface $logger = null, ExposeSecurityLevel $exposeSecurityErrors = ExposeSecurityLevel::AccountStatus)
444+
private function createManager($authenticators, $firewallName = 'main', $eraseCredentials = false, array $requiredBadges = [], ?LoggerInterface $logger = null, ExposeSecurityLevel $exposeSecurityErrors = ExposeSecurityLevel::AccountStatus)
426445
{
427446
return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $firewallName, $logger, $eraseCredentials, $exposeSecurityErrors, $requiredBadges);
428447
}

Tests/EventListener/PasswordMigratingListenerTest.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,6 @@ public function loadUserByUsername(string $username): UserInterface
150150
abstract class TestPasswordAuthenticatedUser implements UserInterface, PasswordAuthenticatedUserInterface
151151
{
152152
abstract public function getPassword(): ?string;
153-
154-
abstract public function getSalt(): ?string;
155153
}
156154

157155
class DummyTestPasswordAuthenticatedUser extends TestPasswordAuthenticatedUser
@@ -161,16 +159,12 @@ public function getPassword(): ?string
161159
return null;
162160
}
163161

164-
public function getSalt(): ?string
165-
{
166-
return null;
167-
}
168-
169162
public function getRoles(): array
170163
{
171164
return [];
172165
}
173166

167+
#[\Deprecated]
174168
public function eraseCredentials(): void
175169
{
176170
}

Tests/Firewall/ContextListenerTest.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -563,21 +563,11 @@ public function __serialize(): array
563563
return [$this->user, $this->roles];
564564
}
565565

566-
public function serialize(): string
567-
{
568-
return serialize($this->__serialize());
569-
}
570-
571566
public function __unserialize(array $data): void
572567
{
573568
[$this->user, $this->roles] = $data;
574569
}
575570

576-
public function unserialize($serialized)
577-
{
578-
$this->__unserialize(\is_array($serialized) ? $serialized : unserialize($serialized));
579-
}
580-
581571
public function __toString(): string
582572
{
583573
return $this->user->getUserIdentifier();
@@ -603,6 +593,7 @@ public function getUserIdentifier(): string
603593
return $this->getUserIdentifier();
604594
}
605595

596+
#[\Deprecated]
606597
public function eraseCredentials(): void
607598
{
608599
}

Tests/LoginLink/LoginLinkHandlerTest.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,16 +284,12 @@ public function getPassword(): string
284284
return $this->passwordProperty;
285285
}
286286

287-
public function getSalt(): string
288-
{
289-
return '';
290-
}
291-
292287
public function getUserIdentifier(): string
293288
{
294289
return $this->username;
295290
}
296291

292+
#[\Deprecated]
297293
public function eraseCredentials(): void
298294
{
299295
}

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"symfony/http-kernel": "^6.4|^7.0",
2323
"symfony/polyfill-mbstring": "~1.0",
2424
"symfony/property-access": "^6.4|^7.0",
25-
"symfony/security-core": "^7.2",
25+
"symfony/security-core": "^7.3",
2626
"symfony/service-contracts": "^2.5|^3"
2727
},
2828
"require-dev": {

0 commit comments

Comments
 (0)