8000 [Security/Http] Throw AccessDeniedException in UserValueResolver when… · symfony/symfony@a5af2cc · GitHub
[go: up one dir, main page]

Skip to content

Commit a5af2cc

Browse files
committed
[Security/Http] Throw AccessDeniedException in UserValueResolver when required but not authenticated
When you add `#[CurrentUser] UserInterface $user` to a controller and the user is not authenticated, the following error is produced: ``` Cannot autowire argument $user of "App\Controller::myAction()": it references class "App\User" but no such service exists. ``` This error doesn't help with resolving the problem. The `UserValueResolver` should check and see if the argument is non-optional and immediately stop execution by throwing an `AccessDeniedException`. This can help redirect the user to a login page.
1 parent 11a87ad commit a5af2cc

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

src/Symfony/Component/Security/Http/Controller/UserValueResolver.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
1717
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1818
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
19+
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
1920
use Symfony\Component\Security\Core\User\UserInterface;
2021

2122
/**
@@ -41,17 +42,41 @@ public function supports(Request $request, ArgumentMetadata $argument): bool
4142

4243
$token = $this->tokenStorage->getToken();
4344
if (!$token instanceof TokenInterface) {
45+
$this->maybeThrowAccessDeniedException($argument);
46+
4447
return false;
4548
}
4649

4750
$user = $token->getUser();
4851

4952
// in case it's not an object we cannot do anything with it; E.g. "anon."
50-
return $user instanceof UserInterface;
53+
if (!$user instanceof UserInterface) {
54+
$this->maybeThrowAccessDeniedException($argument);
55+
56+
return false;
57+
}
58+
59+
return true;
5160
}
5261

5362
public function resolve(Request $request, ArgumentMetadata $argument): iterable
5463
{
5564
yield $this->tokenStorage->getToken()->getUser();
5665
}
66+
67+
private function maybeThrowAccessDeniedException(ArgumentMetadata $argument) : void
68+
{
69+
if ($argument->hasDefaultValue() || (null !== $argument->getType() && $argument->isNullable())) {
70+
return;
71+
}
72+
73+
// Although not really the responsibility of an ArgumentValueResolverInterface, we need to stop here
74+
// because otherwise another resolver (like ServiceValueResolver) can try to load the User class
75+
// from the service container and fail with an exception that is counter-intuitive:
76+
//
77+
// Example: Cannot autowire argument $user of "App\Controller::myAction()": it references class "App\User" but no such service exists.
78+
//
79+
// By throwing an AccessDeniedException we can redirect the user to a login page.
80+
throw new AccessDeniedException('No token found in the security context.');
81+
}
5782
}

src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,52 @@
1818
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
1919
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
2020
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
21+
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
2122
use Symfony\Component\Security\Core\User\UserInterface;
2223
use Symfony\Component\Security\Http\Controller\UserValueResolver;
2324

2425
class UserValueResolverTest extends TestCase
2526
{
26-
public function testResolveNoToken()
27+
public function testResolveNoTokenWhenArgumentIsNullable()
2728
{
2829
$tokenStorage = new TokenStorage();
2930
$resolver = new UserValueResolver($tokenStorage);
30-
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null);
31+
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null, true);
3132

3233
$this->assertFalse($resolver->supports(Request::create('/'), $metadata));
3334
}
3435

36+
public function testResolveNoTokenWhenArgumentHasDefaultValue()
37+
{
38+
$tokenStorage = new TokenStorage();
39+
$resolver = new UserValueResolver($tokenStorage);
40+
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, true, null, true);
41+
42+
$this->assertFalse($resolver->supports(Request::create('/'), $metadata));
43+
}
44+
45+
public function testResolveNoTokenWhenArgumentIsNotNullable()
46+
{
47+
$tokenStorage = new TokenStorage();
48+
$resolver = new UserValueResolver($tokenStorage);
49+
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null, false);
50+
51+
$this->expectException(AccessDeniedException::class);
52+
53+
$resolver->supports(Request::create('/'), $metadata);
54+
}
55+
56+
public function testResolveNoTokenWhenArgumentDoesNotHaveDefaultValue()
57+
{
58+
$tokenStorage = new TokenStorage();
59+
$resolver = new UserValueResolver($tokenStorage);
60+
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null, false);
61+
62+
$this->expectException(AccessDeniedException::class);
63+
64+
$resolver->supports(Request::create('/'), $metadata);
65+
}
66+
3567
public function testResolveNoUser()
3668
{
3769
$mock = $this->createMock(UserInterface::class);

0 commit comments

Comments
 (0)
0