Skip to content

Commit 9fd4503

Browse files
committed
feature symfony#60395 [HttpFoundation] Add #[IsSignatureValid] attribute (santysisi)
This PR was merged into the 7.4 branch. Discussion ---------- [HttpFoundation] Add `#[IsSignatureValid]` attribute | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | Feature symfony#60189 | License | MIT ## ✨ New Feature: `#[IsSignatureValid]` Attribute This attribute enables declarative signature validation on controller actions. It ensures that requests using specific HTTP methods are validated using `UriSigner`. If the signature is invalid, a `SignedUriException` is thrown. ## ✅ Usage Examples ```php #[IsSignatureValid] // Applies to all HTTP methods by default public function someAction(): Response ``` ```php #[IsSignatureValid(methods: ['POST', 'PUT'])] // Only validates POST and PUT requests public function updateAction(): Response ``` ```php #[IsSignatureValid(signer: 'my_custom_signer_service')] // Uses a custom UriSigner service public function customSignedAction(): Response ``` Commits ------- b3a992d [HttpKernel] Add `#[IsSignatureValid]` attribute
2 parents 3ef51cc + b3a992d commit 9fd4503

File tree

14 files changed

+474
-0
lines changed

14 files changed

+474
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Add `assertEmailAddressNotContains()` to the `MailerAssertionsTrait`
1212
* Add `framework.type_info.aliases` option
1313
* Add `KernelBrowser::getSession()`
14+
* Add autoconfiguration tag `kernel.uri_signer` to `Symfony\Component\HttpFoundation\UriSigner`
1415

1516
7.3
1617
---

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class UnusedTagsPass implements CompilerPassInterface
6161
'kernel.fragment_renderer',
6262
'kernel.locale_aware',
6363
'kernel.reset',
64+
'kernel.uri_signer',
6465
'ldap',
6566
'mailer.transport_factory',
6667
'messenger.bus',

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,15 @@
101101
use Symfony\Component\HttpClient\ThrottlingHttpClient;
102102
use Symfony\Component\HttpClient\UriTemplateHttpClient;
103103
use Symfony\Component\HttpFoundation\Request;
104+
use Symfony\Component\HttpFoundation\UriSigner;
104105
use Symfony\Component\HttpKernel\Attribute\AsController;
105106
use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver;
106107
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
107108
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
108109
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
109110
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
110111
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
112+
use Symfony\Component\HttpKernel\EventListener\IsSignatureValidAttributeListener;
111113
use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator;
112114
use Symfony\Component\HttpKernel\Profiler\ProfilerStateChecker;
113115
use Symfony\Component\JsonStreamer\Attribute\JsonStreamable;
@@ -288,6 +290,9 @@ public function load(array $configs, ContainerBuilder $container): void
288290
if (!class_exists(RunProcessMessageHandler::class)) {
289291
$container->removeDefinition('process.messenger.process_message_handler');
290292
}
293+
if (!class_exists(IsSignatureValidAttributeListener::class)) {
294+
$container->removeDefinition('controller.is_signature_valid_attribute_listener');
295+
}
291296

292297
if ($this->hasConsole()) {
293298
$loader->load('console.php');
@@ -762,6 +767,8 @@ public function load(array $configs, ContainerBuilder $container): void
762767
->addTag('mime.mime_type_guesser');
763768
$container->registerForAutoconfiguration(LoggerAwareInterface::class)
764769
->addMethodCall('setLogger', [new Reference('logger')]);
770+
$container->registerForAutoconfiguration(UriSigner::class)
771+
->addTag('kernel.uri_signer');
765772

766773
$container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \ReflectionClass|\ReflectionMethod $reflector) {
767774
$tagAttributes = get_object_vars($attribute);

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Symfony\Component\HttpKernel\EventListener\CacheAttributeListener;
3333
use Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener;
3434
use Symfony\Component\HttpKernel\EventListener\ErrorListener;
35+
use Symfony\Component\HttpKernel\EventListener\IsSignatureValidAttributeListener;
3536
use Symfony\Component\HttpKernel\EventListener\LocaleListener;
3637
use Symfony\Component\HttpKernel\EventListener\ResponseListener;
3738
use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener;
@@ -148,6 +149,13 @@
148149
->tag('kernel.event_subscriber')
149150
->tag('kernel.reset', ['method' => '?reset'])
150151

152+
->set('controller.is_signature_valid_attribute_listener', IsSignatureValidAttributeListener::class)
153+
->args([
154+
service('uri_signer'),
155+
tagged_locator('kernel.uri_signer'),
156+
])
157+
->tag('kernel.event_subscriber')
158+
151159
->set('controller.helper', ControllerHelper::class)
152160
->tag('container.service_subscriber')
153161

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
8+
* Add `#[WithHttpStatus]` to define status codes: 404 for `SignedUriException` and 403 for `ExpiredSignedUriException`
89
* Add support for the `QUERY` HTTP method
910
* Add support for structured MIME suffix
1011

src/Symfony/Component/HttpFoundation/Exception/ExpiredSignedUriException.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111

1212
namespace Symfony\Component\HttpFoundation\Exception;
1313

14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
16+
1417
/**
1518
* @author Kevin Bond <[email protected]>
1619
*/
20+
#[WithHttpStatus(Response::HTTP_FORBIDDEN)]
1721
final class ExpiredSignedUriException extends SignedUriException
1822
{
1923
/**

src/Symfony/Component/HttpFoundation/Exception/SignedUriException.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111

1212
namespace Symfony\Component\HttpFoundation\Exception;
1313

14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
16+
1417
/**
1518
* @author Kevin Bond <[email protected]>
1619
*/
20+
#[WithHttpStatus(Response::HTTP_NOT_FOUND)]
1721
abstract class SignedUriException extends \RuntimeException implements ExceptionInterface
1822
{
1923
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\HttpKernel\Attribute;
13+
14+
/**
15+
* Validates the request signature for specific HTTP methods.
16+
*
17+
* This class determines whether a request's signature should be validated
18+
* based on the configured HTTP methods. If the request method matches one
19+
* of the specified methods (or if no methods are specified), the signature
20+
* is checked.
21+
*
22+
* If the signature is invalid, a {@see \Symfony\Component\HttpFoundation\Exception\SignedUriException}
23+
* is thrown during validation.
24+
*
25+
* @author Santiago San Martin <[email protected]>
26+
*/
27+
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
28+
final class IsSignatureValid
29+
{
30+
/** @var string[] */
31+
public readonly array $methods;
32+
public readonly ?string $signer;
33+
34+
/**
35+
* @param string[]|string $methods HTTP methods that require signature validation. An empty array means that no method filtering is done
36+
* @param string $signer The ID of the UriSigner service to use for signature validation. Defaults to 'uri_signer'
37+
*/
38+
public function __construct(
39+
array|string $methods = [],
40+
?string $signer = null,
41+
) {
42+
$this->methods = (array) $methods;
43+
$this->signer = $signer;
44+
}
45+
}

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add support for the `QUERY` HTTP method
88
* Deprecate implementing `__sleep/wakeup()` on kernels; use `__(un)serialize()` instead
99
* Deprecate implementing `__sleep/wakeup()` on data collectors; use `__(un)serialize()` instead
10+
* Add `#[IsSignatureValid]` attribute to validate URI signatures
1011
* Make `Profile` final and `Profiler::__sleep()` internal
1112

1213
7.3
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\HttpKernel\EventListener;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\HttpFoundation\UriSigner;
17+
use Symfony\Component\HttpKernel\Attribute\IsSignatureValid;
18+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
19+
use Symfony\Component\HttpKernel\KernelEvents;
20+
21+
/**
22+
* Handles the IsSignatureValid attribute.
23+
*
24+
* @author Santiago San Martin <[email protected]>
25+
*/
26+
class IsSignatureValidAttributeListener implements EventSubscriberInterface
27+
{
28+
public function __construct(
29+
private readonly UriSigner $uriSigner,
30+
private readonly ContainerInterface $container,
31+
) {
32+
}
33+
34+
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
35+
{
36+
if (!$attributes = $event->getAttributes(IsSignatureValid::class)) {
37+
return;
38+
}
39+
40+
$request = $event->getRequest();
41+
foreach ($attributes as $attribute) {
42+
$methods = array_map('strtoupper', $attribute->methods);
43+
if ($methods && !\in_array($request->getMethod(), $methods, true)) {
44+
continue;
45+
}
46+
47+
if (null === $attribute->signer) {
48+
$this->uriSigner->verify($request);
49+
continue;
50+
}
51+
52+
$signer = $this->container->get($attribute->signer);
53+
if (!$signer instanceof UriSigner) {
54+
throw new \LogicException(\sprintf('The service "%s" is not an instance of "%s".', $attribute->signer, UriSigner::class));
55+
}
56+
57+
$signer->verify($request);
58+
}
59+
}
60+
61+
public static function getSubscribedEvents(): array
62+
{
63+
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 30]];
64+
}
65+
}

0 commit comments

Comments
 (0)