8000 with factory · symfony/symfony@32b95c8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 32b95c8

Browse files
committed
with factory
1 parent 707245c commit 32b95c8

File tree

9 files changed

+397
-30
lines changed

9 files changed

+397
-30
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
}
63+
}

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;
@@ -89,6 +90,27 @@ public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient()
8990
$this->assertFalse($container->hasDefinition('http_client.security.access_token_handler.oidc_user_info'));
9091
}
9192

93+
public function testCasTokenHandlerConfiguration()
94+
{
95+
$container = new ContainerBuilder();
96+
$config = [
97+
'token_handler' => ['cas' => ['validation_url' => 'https://www.example.com/cas/validate']],
98+
];
99+
100+
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
101+
$finalizedConfig = $this->processConfig($config, $factory);
102+
103+
$factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
104+
105+
$this->assertTrue($container->hasDefinition('security.access_token_handler.cas'));
106+
107+
$arguments = $container->getDefinition('security.access_token_handler.cas')->getArguments();
108+
$this->assertSame((string) $arguments[0], 'request_stack');
109+
$this->assertSame($arguments[1], 'https://www.example.com/cas/validate');
110+
$this->assertSame($arguments[2], 'cas');
111+
$this->assertNull($arguments[3]);
112+
}
113+
92114
public function testOidcUserInfoTokenHandlerConfigurationWithClientCreation()
93115
{
94116
$container = new ContainerBuilder();
@@ -180,6 +202,7 @@ private function createTokenHandlerFactories(): array
180202
new ServiceTokenHandlerFactory(),
181203
new OidcUserInfoTokenHandlerFactory(),
182204
new OidcTokenHandlerFactory(),
205+
new CasTokenHandlerFactory(),
183206
];
184207
}
185208
}

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

Lines changed: 55 additions & 30 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
@@ -333,38 +335,61 @@ public function testSelfContainedTokens()
333335
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
334336
}
335337

336-
/**
337-
* @requires extension openssl
338-
*/
339-
public function testOidcSuccess()
340-
{
341-
$time = time();
342-
$claims = [
343-
'iat' => $time,
344-
'nbf' => $time,
345-
'exp' => $time + 3600,
346-
'iss' => 'https://www.example.com/',
347-
'aud' => 'Symfony OIDC',
348-
'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
349-
'username' => 'dunglas',
350-
];
351-
$token = (new CompactSerializer())->serialize((new JWSBuilder(new AlgorithmManager([
352-
new ES256(),
353-
])))->create()
354-
->withPayload(json_encode($claims))
355-
// tip: use https://mkjwk.org/ to generate a JWK
356-
->addSignature(new JWK([
357-
'kty' => 'EC',
358-
'crv' => 'P-256',
359-
'x' => '0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4',
360-
'y' => 'KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo',
361-
'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220',
362-
]), ['alg' => 'ES256'])
363-
->build()
338+
/**
339+
* @requires extension openssl
340+
*/
341+
public function testOidcSuccess()
342+
{
343+
$time = time();
344+
$claims = [
345+
'iat' => $time,
346+
'nbf' => $time,
347+
'exp' => $time + 3600,
348+
'iss' => 'https://www.example.com/',
349+
'aud' => 'Symfony OIDC',
350+
'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
351+
'username' => 'dunglas',
352+
];
353+
$token = (new CompactSerializer())->serialize((new JWSBuilder(new AlgorithmManager([
354+
new ES256(),
355+
])))->create()
356+
->withPayload(json_encode($claims))
357+
// tip: use https://mkjwk.org/ to generate a JWK
358+
->addSignature(new JWK([
359+
'kty' => 'EC',
360+
'crv' => 'P-256',
361+
'x' => '0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4',
362+
'y' => 'KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo',
363+
'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220',
364+
]), ['alg' => 'ES256'])
365+
->build()
366+
);
367+
368+
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_oidc.yml']);
369+
$client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => sprintf('Bearer %s', $token)]);
370+
$response = $client->getResponse();
371+
372+
$this->assertInstanceOf(Response::class, $response);
373+
$this->assertSame(200, $response->getStatusCode());
374+
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
375+
}
376+
377+
public function testCasSuccess()
378+
{
379+
$casResponse = new MockResponse(<<<BODY
380+
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
381+
<cas:authenticationSuccess>
382+
<cas:user>dunglas</cas:user>
383+
<cas:proxyGrantingTicket>PGTIOU-84678-8a9d</cas:proxyGrantingTicket>
384+
</cas:authenticationSuccess>
385+
</cas:serviceResponse>
386+
BODY
364387
);
365388

366-
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_oidc.yml']);
367-
$client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => sprintf('Bearer %s', $token)]);
389+
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_cas.yml']);
390+
$client->getContainer()->set('Symfony\Contracts\HttpClient\HttpClientInterface', new MockHttpClient($casResponse));
391+
392+
$client->request('GET', '/foo?ticket=PGTIOU-84678-8a9d', [], [], []);
368393
$response = $client->getResponse();
369394

370395
$this->assertInstanceOf(Response::class, $response);
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
@@ -12,6 +12,7 @@ CHANGELOG
1212
* Call `UserBadge::userLoader` with attributes if the argument is set
1313
* Allow to override badge fqcn on `Passport::addBadge`
1414
* Add `SecurityTokenValueResolver` to inject token as controller argument
15+
* Add CAS 2.0 access token handler
1516

1617
6.2
1718
---

0 commit comments

Comments
 (0)
0