8000 wip: refacto for 6.4 · symfony/symfony@f1d7218 · GitHub
[go: up one dir, main page]

Skip to content

Commit f1d7218

Browse files
committed
wip: refacto for 6.4
1 parent 7d310a3 commit f1d7218

File tree

9 files changed

+363
-0
lines changed

9 files changed

+363
-0
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\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken;
13+
14+
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
15+
use Symfony\Component\DependencyInjection\ChildDefinition;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\Security\Http\AccessToken\Cas\Cas2Handler;
19+
20+
class CasTokenHandlerFactory implements TokenHandlerFactoryInterface
21+
{
22+
public function create(ContainerBuilder $container, string $id, array|string $config): void
23+
{
24+
$container->setDefinition($id, new ChildDefinition('security.access_token_handler.cas'));
25+
26+
$container
27+
->register('security.access_token_handler.cas', Cas2Handler::class)
28+
->setArguments([
29+
new Reference('request_stack'),
30+
$config['validation_url'],
31+
$config['prefix'],
32+
$config['http_client'] ? new Reference($config['http_client']) : null,
33+
]);
34+
}
35+
36+
public function getKey(): string
37+
{
38+
return 'cas';
39+
}
40+
41+
public function addConfiguration(NodeBuilder $node): void
42+
{
43+
$node
44+
->arrayNode($this->getKey())
45+
->fixXmlConfig($this->getKey())
46+
->children()
47+
->scalarNode('validation_url')
48+
->info('CAS server validation URL')
49+
->isRequired()
50+
->end()
51+
->scalarNode('prefix')
52+
->info('CAS prefix')
53+
->defaultValue('cas')
54+
->end()
55+
->scalarNode('http_client')
56+
->info('HTTP Client service')
57+
->defaultNull()
58+
->end()
59+
->end()
60+
->end();
61+
}
62+
}

src/Symfony/Bundle/SecurityBundle/SecurityBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass;
2424
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\ReplaceDecoratedRememberMeHandlerPass;
2525
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass;
26+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory;
2627
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory;
2728
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory;
2829
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory;
@@ -81,6 +82,7 @@ public function build(ContainerBuilder $container)
8182
new ServiceTokenHandlerFactory(),
8283
new OidcUserInfoTokenHandlerFactory(),
8384
new OidcTokenHandlerFactory(),
85+
new CasTokenHandlerFactory(),
8486
]));
8587

8688
$extension->addUserProviderFactory(new InMemoryFactory());

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory;
1516
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory;
1617
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory;
1718
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory;
@@ -76,6 +77,27 @@ public function testIdTokenHandlerConfiguration()
7677
$this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
7778
}
7879

80+
public function testCasTokenHandlerConfiguration()
81+
{
82+
$container = new ContainerBuilder();
83+
$config = [
84+
'token_handler' => ['cas' => ['validation_url' => 'https://www.example.com/cas/validate']],
85+
];
86+
87+
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
88+
$finalizedConfig = $this->processConfig($config, $factory);
89+
90+
$factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
91+
92+
$this->assertTrue($container->hasDefinition('security.access_token_handler.cas'));
93+
94+
$arguments = $container->getDefinition('security.access_token_handler.cas')->getArguments();
95+
$this->assertSame((string) $arguments[0], 'request_stack');
96+
$this->assertSame($arguments[1], 'https://www.example.com/cas/validate');
97+
$this->assertSame($arguments[2], 'cas');
98+
$this->assertNull($arguments[3]);
99+
}
100+
79101
public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient()
80102
{
81103
$container = new ContainerBuilder();
@@ -217,6 +239,7 @@ private function createTokenHandlerFactories(): array
217239
new ServiceTokenHandlerFactory(),
218240
new OidcUserInfoTokenHandlerFactory(),
219241
new OidcTokenHandlerFactory(),
242+
new CasTokenHandlerFactory(),
220243
];
221244
}
222245
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Jose\Component\Signature\JWSBuilder;
1818
use Jose\Component\Signature\Serializer\CompactSerializer;
1919
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
20+
use Symfony\Component\HttpClient\MockHttpClient;
21+
use Symfony\Component\HttpClient\Response\MockResponse;
2022
use Symfony\Component\HttpFoundation\Response;
2123

2224
class AccessTokenTest extends AbstractWebTestCase
@@ -383,4 +385,27 @@ public function testOidcSuccess()
383385
$this->assertSame(200, $response->getStatusCode());
384386
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
385387
}
388+
389+
public function testCasSuccess()
390+
{
391+
$casResponse = new MockResponse(<<<BODY
392+
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
393+
<cas:authenticationSuccess>
394+
<cas:user>dunglas</cas:user>
395+
<cas:proxyGrantingTicket>PGTIOU-84678-8a9d</cas:proxyGrantingTicket>
396+
</cas:authenticationSuccess>
397+
</cas:serviceResponse>
398+
BODY
399+
);
400+
401+
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_cas.yml']);
402+
$client->getContainer()->set('Symfony\Contracts\HttpClient\HttpClientInterface', new MockHttpClient($casResponse));
403+
404+
$client->request('GET', '/foo?ticket=PGTIOU-84678-8a9d', [], [], []);
405+
$response = $client->getResponse();
406+
407+
$this->assertInstanceOf(Response::class, $response);
408+
$this->assertSame(200, $response->getStatusCode());
409+
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
410+
}
386411
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
imports:
2+
- { resource: ./../config/framework.yml }
3+
4+
framework:
5+
http_method_override: false
6+
serializer: ~
7+
8+
security:
9+
password_hashers:
10+
Symfony\Component\Security\Core\User\InMemoryUser: plaintext
11+
12+
providers:
13+
in_memory:
14+
memory:
15+
users:
16+
dunglas: { password: foo, roles: [ROLE_USER] }
17+
18+
firewalls:
19+
main:
20+
pattern: ^/
21+
access_token:
22+
token_handler:
23+
cas:
24+
validation_url: 'https://www.example.com/cas/serviceValidate'
25+
http_client: 'Symfony\Contracts\HttpClient\HttpClientInterface'
26+
token_extractors:
27+
- security.access_token_extractor.cas
28+
29+
access_control:
30+
- { path: ^/foo, roles: ROLE_USER }
31+
32+
services:
33+
_defaults:
34+
public: true
35+
36+
security.access_token_extractor.cas:
37+
class: Symfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor
38+
arguments:
39+
- 'ticket'
40+
41+
Symfony\Contracts\HttpClient\HttpClientInterface: ~
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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\Component\Security\Http\AccessToken\Cas;
13+
14+
use Symfony\Component\HttpClient\HttpClient;
15+
use Symfony\Component\HttpFoundation\RequestStack;
16+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
17+
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
18+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
19+
use Symfony\Contracts\HttpClient\HttpClientInterface;
20+
21+
/**
22+
* @see https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-V2-Specification.html
23+
*
24+
* @author Nicolas Attard <contact@nicolasattard.fr>
25+
*/
26+
final class Cas2Handler implements AccessTokenHandlerInterface
27+
{
28+
public function __construct(
29+
private readonly RequestStack $requestStack,
30+
private readonly string $validationUrl,
31+
private readonly string $prefix = 'cas',
32+
private ?HttpClientInterface $client = null,
33+
) {
34+
if (null === $client) {
35+
if (!class_exists(HttpClient::class)) {
36+
throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
37+
}
38+
39+
$this->client = HttpClient::create();
40+
}
41+
}
42+
43+
/**
44+
* @throws AuthenticationException
45+
*/
46+
public function getUserBadgeFrom(string $accessToken): UserBadge
47+
{
48+
$response = $this->client->request('GET', $this->getvalidationUrl($accessToken));
49+
50+
$xml = new \SimpleXMLElement($response->getContent(), 0, false, $this->prefix, true);
51+
52+
if (isset($xml->authenticationSuccess)) {
53+
return new UserBadge((string) $xml->authenticationSuccess->user);
54+
}
55+
56+
if (isset($xml->authenticationFailure)) {
57+
throw new AuthenticationException('CAS Authentication Failure: '.trim((string) $xml->authenticationFailure));
58+
}
59+
60+
throw new AuthenticationException('Invalid CAS response.');
61+
}
62+
63+
private function getValidationUrl(string $accessToken): string
64+
{
65+
$request = $this->requestStack->getCurrentRequest();
66+
67+
if (null === $request) {
68+
throw new \LogicException('Request should exist so it can be processed for error.');
69+
}
70+
71+
$query = $request->query->all();
72+
73+
if (!isset($query['ticket'])) {
74+
throw new AuthenticationException('No ticket found in request.');
75+
}
76+
unset($query['ticket']);
77+
$queryString = empty($query) ? '' : '?'.http_build_query($query);
78+
79+
return sprintf('%s?ticket=%s&service=%s',
80+
$this->validationUrl,
81+
urlencode($accessToken),
82+
urlencode($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$queryString)
83+
);
84+
}
85+
}

src/Symfony/Component/Security/Http/CHANGELOG.md

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

77
* `UserValueResolver` no longer implements `ArgumentValueResolverInterface`
8+
* Add CAS 2.0 access token handler
89

910
6.3
1011
---

0 commit comments

Comments
 (0)
0