8000 [Security] Rework the remember me system · symfony/symfony@1567041 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1567041

Browse files
wouterjchalasr
authored andcommitted
[Security] Rework the remember me system
1 parent 0f96ac7 commit 1567041

File tree

68 files changed

+2240
-505
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2240
-505
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\SchemaListener;
13+
14+
use Doctrine\Common\EventSubscriber;
15+
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
16+
use Doctrine\ORM\Tools\ToolEvents;
17+
use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider;
18+
use Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler;
19+
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
20+
21+
/**
22+
* Automatically adds the rememberme table needed for the {@see DoctrineTokenProvider}.
23+
*
24+
* @author Wouter de Jong <wouter@wouterj.nl>
25+
*/
26+
final class RememberMeTokenProviderDoctrineSchemaSubscriber implements EventSubscriber
27+
{
28+
private $rememberMeHandlers;
29+
30+
/**
31+
* @param iterable|RememberMeHandlerInterface[] $rememberMeHandlers
32+
*/
33+
public function __construct(iterable $rememberMeHandlers)
34+
{
35+
$this->rememberMeHandlers = $rememberMeHandlers;
36+
}
37+
38+
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
39+
{
40+
$dbalConnection = $event->getEntityManager()->getConnection();
41+
42+
foreach ($this->rememberMeHandlers as $rememberMeHandler) {
43+
if (
44+
$rememberMeHandler instanceof PersistentRememberMeHandler
45+
&& ($tokenProvider = $rememberMeHandler->getTokenProvider()) instanceof DoctrineTokenProvider
46+
) {
47+
$tokenProvider->configureSchema($event->getSchema(), $dbalConnection);
48+
}
49+
}
50+
}
51+
52+
public function getSubscribedEvents(): array
53+
{
54+
if ( 1CF5 !class_exists(ToolEvents::class)) {
55+
return [];
56+
}
57+
58+
return [
59+
ToolEvents::postGenerateSchema,
60+
];
61+
}
62+
}

src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
use Doctrine\DBAL\Connection;
1515
use Doctrine\DBAL\Driver\Result as DriverResult;
1616
use Doctrine\DBAL\Result;
17+
use Doctrine\DBAL\Schema\Schema;
1718
use Doctrine\DBAL\Types\Types;
1819
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
1920
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface;
2021
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
2122
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
2223

2324
/**
24-
* This class provides storage for the tokens that is set in "remember me"
25+
* This class provides storage for the tokens that is set in "remember-me"
2526
* cookies. This way no password secrets will be stored in the cookies on
2627
* the client machine, and thus the security is improved.
2728
*
@@ -53,8 +54,7 @@ public function __construct(Connection $conn)
5354
public function loadTokenBySeries(string $series)
5455
{
5556
// the alias for lastUsed works around case insensitivity in PostgreSQL
56-
$sql = 'SELECT class, username, value, lastUsed AS last_used'
57-
.' FROM rememberme_token WHERE series=:series';
57+
$sql = 'SELECT class, username, value, lastUsed AS last_used FROM rememberme_token WHERE series=:series';
5858
$paramValues = ['series' => $series];
5959
$paramTypes = ['series' => \PDO::PARAM_STR];
6060
$stmt = $this->conn->executeQuery($sql, $paramValues, $paramTypes);
@@ -87,8 +87,7 @@ public function deleteTokenBySeries(string $series)
8787
*/
8888
public function updateToken(string $series, string $tokenValue, \DateTime $lastUsed)
8989
{
90-
$sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed'
91-
.' WHERE series=:series';
90+
$sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed WHERE series=:series';
9291
$paramValues = [
9392
'value' => $tokenValue,
9493
'lastUsed' => $lastUsed,
@@ -114,9 +113,7 @@ public function updateToken(string $series, string $tokenValue, \DateTime $lastU
114113
*/
115114
public function createNewToken(PersistentTokenInterface $token)
116115
{
117-
$sql = 'INSERT INTO rememberme_token'
118-
.' (class, username, series, value, lastUsed)'
119-
.' VALUES (:class, :username, :series, :value, :lastUsed)';
116+
$sql = 'INSERT INTO rememberme_token (class, username, series, value, lastUsed) VALUES (:class, :username, :series, :value, :lastUsed)';
120117
$paramValues = [
121118
'class' => $token->getClass(),
122119
// @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0
@@ -138,4 +135,32 @@ public function createNewToken(PersistentTokenInterface $token)
138135
$this->conn->executeUpdate($sql, $paramValues, $paramTypes);
139136
}
140137
}
138+
139+
/**
140+
* Adds the Table to the Schema if "remember me" uses this Connection.
141+
*/
142+
public function configureSchema(Schema $schema, Connection $forConnection): void
143+
{
144+
// only update the schema for this connection
145+
if ($forConnection !== $this->conn) {
146+
return;
147+
}
148+
149+
if ($schema->hasTable('rememberme_token')) {
150+
return;
151+
}
152+
153+
$this->addTableToSchema($schema);
154+
}
155+
156+
private function addTableToSchema(Schema $schema): void
157+
{
158+
$table = $schema->createTable('rememberme_token');
159+
$table->addColumn('series', Types::STRING, ['length' => 88]);
160+
$table->addColumn('value', Types::STRING, ['length' => 88]);
161+
$table->addColumn('lastUsed', Types::DATETIME_MUTABLE);
162+
$table->addColumn('class', Types::STRING, ['length' => 100]);
163+
$table->addColumn('username', Types::STRING, ['length' => 200]);
164+
$table->setPrimaryKey(['series']);
165+
}
141166
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class UnusedTagsPass implements CompilerPassInterface
7777
'security.authenticator.login_linker',
7878
'security.expression_language_provider',
7979
'security.remember_me_aware',
80+
'security.remember_me_handler',
8081
'security.voter',
8182
'serializer.encoder',
8283
'serializer.normalizer',

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterGlobalSecurityEventListenersPass.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
2222
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
2323
use Symfony\Component\Security\Http\Event\LogoutEvent;
24+
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
2425
use Symfony\Component\Security\Http\SecurityEvents;
2526

2627
/**
@@ -44,6 +45,7 @@ class RegisterGlobalSecurityEventListenersPass implements CompilerPassInterface
4445
AuthenticationTokenCreatedEvent::class,
4546
AuthenticationSuccessEvent::class,
4647
InteractiveLoginEvent::class,
48+
TokenDeauthenticatedEvent::class,
4749

4850
// When events are registered by their name
4951
AuthenticationEvents::AUTHENTICATION_SUCCESS,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Bundle\SecurityBundle\RememberMe\DecoratedRememberMeHandler;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
18+
/**
19+
* Replaces the DecoratedRememberMeHandler services with the real definition.
20+
*
21+
* @author Wouter de Jong <wouter@wouterj.nl>
22+
*
23+
* @internal
24+
*/
25+
final class ReplaceDecoratedRememberMeHandlerPass implements CompilerPassInterface
26+
{
27+
private const HANDLER_TAG = 'security.remember_me_handler';
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function process(ContainerBuilder $container): void
33+
{
34+
$handledFirewalls = [];
35+
foreach ($container->findTaggedServiceIds(self::HANDLER_TAG) as $definitionId => $rememberMeHandlerTags) {
36+
$definition = $container->findDefinition($definitionId);
37+
if (DecoratedRememberMeHandler::class !== $definition->getClass()) {
38+
continue;
39+
}
40+
41+
// get the actual custom remember me handler definition (passed to the decorator)
42+
$realRememberMeHandler = $container->findDefinition((string) $definition->getArgument(0));
43+
if (null === $realRememberMeHandler) {
44+
throw new \LogicException(sprintf('Invalid service definition for custom remember me handler; no service found with ID "%s".', (string) $definition->getArgument(0)));
45+
}
46+
47+
foreach ($rememberMeHandlerTags as $rememberMeHandlerTag) {
48+
// some custom handlers may be used on multiple firewalls in the same application
49+
if (\in_array($rememberMeHandlerTag['firewall'], $handledFirewalls, true)) {
50+
continue;
51+
}
52+
53+
$rememberMeHandler = clone $realRememberMeHandler;
54+
$rememberMeHandler->addTag(self::HANDLER_TAG, $rememberMeHandlerTag);
55+
$container->setDefinition('security.authenticator.remember_me_handler.'.$rememberMeHandlerTag['firewall'], $rememberMeHandler);
56+
57+
$handledFirewalls[] = $rememberMeHandlerTag['firewall'];
58+
}
59+
}
60+
}
61+
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,24 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
113113
->replaceArgument(1, $config['lifetime']);
114114
}
115115

116+
$signatureHasherId = 'security.authenticator.login_link_signature_hasher.'.$firewallName;
117+
$container
118+
->setDefinition($signatureHasherId, new ChildDefinition('security.authenticator.abstract_login_link_signature_hasher'))
119+
->replaceArgument(1, $config['signature_properties'])
120+
->replaceArgument(3, $expiredStorageId ? new Reference($expiredStorageId) : null)
121+
->replaceArgument(4, $config['max_uses'] ?? null)
122+
;
123+
116124
$linkerId = 'security.authenticator.login_link_handler.'.$firewallName;
117125
$linkerOptions = [
118126
'route_name' => $config['check_route'],
119127
'lifetime' => $config['lifetime'],
120-
'max_uses' => $config['max_uses'] ?? null,
121128
];
122129
$container
123130
->setDefinition($linkerId, new ChildDefinition('security.authenticator.abstract_login_link_handler'))
124131
->replaceArgument(1, new Reference($userProviderId))
125-
->replaceArgument(3, $config['signature_properties'])
126-
->replaceArgument(5, $linkerOptions)
127-
->replaceArgument(6, $expiredStorageId ? new Reference($expiredStorageId) : null)
132+
->replaceArgument(2, new Reference($signatureHasherId))
133+
->replaceArgument(3, $linkerOptions)
128134
->addTag('security.authenticator.login_linker', ['firewall' => $firewallName])
129135
;
130136

0 commit comments

Comments
 (0)
0