Skip to content

Commit 1b7fbac

Browse files
Kocnicolas-grekas
authored andcommitted
[Security][HttpKernel] Create Controller Argument Resolver for Security Token
1 parent 4df753b commit 1b7fbac

File tree

3 files changed

+156
-0
lines changed

3 files changed

+156
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Add `attributes` optional array argument in `UserBadge`
1212
* Call `UserBadge::userLoader` with attributes if the argument is set
1313
* Allow to override badge fqcn on `Passport::addBadge`
14+
* Add `SecurityTokenValueResolver` to inject token as controller argument
1415

1516
6.2
1617
---
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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\Security\Http\Controller;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
17+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
18+
use Symfony\Component\HttpKernel\Exception\HttpException;
19+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
20+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
21+
22+
/**
23+
* @author Konstantin Myakshin <[email protected]>
24+
*/
25+
final class SecurityTokenValueResolver implements ValueResolverInterface
26+
{
27+
public function __construct(private readonly TokenStorageInterface $tokenStorage)
28+
{
29+
}
30+
31+
/**
32+
* @return TokenInterface[]
33+
*/
34+
public function resolve(Request $request, ArgumentMetadata $argument): array
35+
{
36+
if (!($type = $argument->getType()) || (TokenInterface::class !== $type && !is_subclass_of($type, TokenInterface::class))) {
37+
return [];
38+
}
39+
40+
if (null !== $token = $this->tokenStorage->getToken()) {
41+
return [$token];
42+
}
43+
44+
if ($argument->isNullable()) {
45+
return [];
46+
}
47+
48+
throw new HttpException(Response::HTTP_UNAUTHORIZED, 'A security token is required but the token storage is empty.');
49+
}
50+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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\Security\Http\Tests\Controller;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
17+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
18+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
19+
use Symfony\Component\HttpKernel\Exception\HttpException;
20+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
21+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
22+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
23+
use Symfony\Component\Security\Core\User\InMemoryUser;
24+
use Symfony\Component\Security\Http\Controller\SecurityTokenValueResolver;
25+
26+
class SecurityTokenValueResolverTest extends TestCase
27+
{
28+
public function testResolveSucceedsWithTokenInterface()
29+
{
30+
$user = new InMemoryUser('username', 'password');
31+
$token = new UsernamePasswordToken($user, 'provider');
32+
$tokenStorage = new TokenStorage();
33+
$tokenStorage->setToken($token);
34+
35+
$resolver = new SecurityTokenValueResolver($tokenStorage);
36+
$metadata = new ArgumentMetadata('foo', TokenInterface::class, false, false, null);
37+
38+
$this->assertSame([$token], $resolver->resolve(Request::create('/'), $metadata));
39+
}
40+
41+
public function testResolveSucceedsWithSubclassType()
42+
{
43+
$user = new InMemoryUser('username', 'password');
44+
$token = new UsernamePasswordToken($user, 'provider');
45+
$tokenStorage = new TokenStorage();
46+
$tokenStorage->setToken($token);
47+
48+
$resolver = new SecurityTokenValueResolver($tokenStorage);
49+
$metadata = new ArgumentMetadata('foo', UsernamePasswordToken::class, false, false, null, false);
50+
51+
$this->assertSame([$token], $resolver->resolve(Request::create('/'), $metadata));
52+
}
53+
54+
public function testResolveSucceedsWithNullableParamAndNoToken()
55+
{
56+
$tokenStorage = new TokenStorage();
57+
$resolver = new SecurityTokenValueResolver($tokenStorage);
58+
$metadata = new ArgumentMetadata('foo', TokenInterface::class, false, false, null, true);
59+
60+
$this->assertSame([], $resolver->resolve(Request::create('/'), $metadata));
61+
}
62+
63+
public function testResolveThrowsUnauthenticatedWithNoToken()
64+
{
65+
$tokenStorage = new TokenStorage();
66+
$resolver = new SecurityTokenValueResolver($tokenStorage);
67+
$metadata = new ArgumentMetadata('foo', UsernamePasswordToken::class, false, false, null, false);
68+
69+
$this->expectException(HttpException::class);
70+
$this->expectExceptionMessage('A security token is required but the token storage is empty.');
71+
72+
$resolver->resolve(Request::create('/'), $metadata);
73+
}
74+
75+
public function testIntegration()
76+
{
77+
$user = new InMemoryUser('username', 'password');
78+
$token = new UsernamePasswordToken($user, 'provider');
79+
$tokenStorage = new TokenStorage();
80+
$tokenStorage->setToken($token);
81+
82+
$argumentResolver = new ArgumentResolver(null, [new SecurityTokenValueResolver($tokenStorage)]);
83+
$this->assertSame([$token], $argumentResolver->getArguments(Request::create('/'), static function (TokenInterface $token) {}));
84+
}
85+
86+
public function testIntegrationNoToken()
87+
{
88+
$tokenStorage = new TokenStorage();
89+
90+
$argumentResolver = new ArgumentResolver(null, [new SecurityTokenValueResolver($tokenStorage), new DefaultValueResolver()]);
91+
$this->assertSame([null], $argumentResolver->getArguments(Request::create('/'), static function (?TokenInterface $token) {}));
92+
}
93+
94+
public function testIntegrationNonNullablwWithNoToken()
95+
{
96+
$tokenStorage = new TokenStorage();
97+
98+
$argumentResolver = new ArgumentResolver(null, [new SecurityTokenValueResolver($tokenStorage), new DefaultValueResolver()]);
99+
100+
$this->expectException(HttpException::class);
101+
$this->expectExceptionMessage('A security token is required but the token storage is empty.');
102+
103+
$argumentResolver->getArguments(Request::create('/'), static function (TokenInterface $token) {});
104+
}
105+
}

0 commit comments

Comments
 (0)