Skip to content

Commit

Permalink
wip: refacto for 6.4
Browse files Browse the repository at this point in the history
  • Loading branch information
nacorp committed Sep 21, 2023
1 parent 7d310a3 commit f1d7218
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken;

use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Security\Http\AccessToken\Cas\Cas2Handler;

class CasTokenHandlerFactory implements TokenHandlerFactoryInterface
{
public function create(ContainerBuilder $container, string $id, array|string $config): void
{
$container->setDefinition($id, new ChildDefinition('security.access_token_handler.cas'));

$container
->register('security.access_token_handler.cas', Cas2Handler::class)
->setArguments([
new Reference('request_stack'),
$config['validation_url'],
$config['prefix'],
$config['http_client'] ? new Reference($config['http_client']) : null,
]);
}

public function getKey(): string
{
return 'cas';
}

public function addConfiguration(NodeBuilder $node): void
{
$node
->arrayNode($this->getKey())
->fixXmlConfig($this->getKey())
->children()
->scalarNode('validation_url')
->info('CAS server validation URL')
->isRequired()
->end()
->scalarNode('prefix')

Check failure on line 51 in src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/CasTokenHandlerFactory.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedInterfaceMethod

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/CasTokenHandlerFactory.php:51:15: UndefinedInterfaceMethod: Method Symfony\Component\Config\Definition\Builder\NodeParentInterface::scalarNode does not exist (see https://psalm.dev/181)

Check failure on line 51 in src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/CasTokenHandlerFactory.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedInterfaceMethod

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/CasTokenHandlerFactory.php:51:15: UndefinedInterfaceMethod: Method Symfony\Component\Config\Definition\Builder\NodeParentInterface::scalarNode does not exist (see https://psalm.dev/181)
->info('CAS prefix')
->defaultValue('cas')
->end()
->scalarNode('http_client')
->info('HTTP Client service')
->defaultNull()
->end()
->end()
->end();
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/SecurityBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\ReplaceDecoratedRememberMeHandlerPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory;
Expand Down Expand Up @@ -81,6 +82,7 @@ public function build(ContainerBuilder $container)
new ServiceTokenHandlerFactory(),
new OidcUserInfoTokenHandlerFactory(),
new OidcTokenHandlerFactory(),
new CasTokenHandlerFactory(),
]));

$extension->addUserProviderFactory(new InMemoryFactory());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory;

use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory;
Expand Down Expand Up @@ -76,6 +77,27 @@ public function testIdTokenHandlerConfiguration()
$this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
}

public function testCasTokenHandlerConfiguration()
{
$container = new ContainerBuilder();
$config = [
'token_handler' => ['cas' => ['validation_url' => 'https://www.example.com/cas/validate']],
];

$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
$finalizedConfig = $this->processConfig($config, $factory);

$factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');

$this->assertTrue($container->hasDefinition('security.access_token_handler.cas'));

$arguments = $container->getDefinition('security.access_token_handler.cas')->getArguments();
$this->assertSame((string) $arguments[0], 'request_stack');
$this->assertSame($arguments[1], 'https://www.example.com/cas/validate');
$this->assertSame($arguments[2], 'cas');
$this->assertNull($arguments[3]);
}

public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient()
{
$container = new ContainerBuilder();
Expand Down Expand Up @@ -217,6 +239,7 @@ private function createTokenHandlerFactories(): array
new ServiceTokenHandlerFactory(),
new OidcUserInfoTokenHandlerFactory(),
new OidcTokenHandlerFactory(),
new CasTokenHandlerFactory(),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpFoundation\Response;

class AccessTokenTest extends AbstractWebTestCase
Expand Down Expand Up @@ -383,4 +385,27 @@ public function testOidcSuccess()
$this->assertSame(200, $response->getStatusCode());
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
}

public function testCasSuccess()
{
$casResponse = new MockResponse(<<<BODY
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>dunglas</cas:user>
<cas:proxyGrantingTicket>PGTIOU-84678-8a9d</cas:proxyGrantingTicket>
</cas:authenticationSuccess>
</cas:serviceResponse>
BODY
);

$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_cas.yml']);
$client->getContainer()->set('Symfony\Contracts\HttpClient\HttpClientInterface', new MockHttpClient($casResponse));

$client->request('GET', '/foo?ticket=PGTIOU-84678-8a9d', [], [], []);
$response = $client->getResponse();

$this->assertInstanceOf(Response::class, $response);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
imports:
- { resource: ./../config/framework.yml }

framework:
http_method_override: false
serializer: ~

security:
password_hashers:
Symfony\Component\Security\Core\User\InMemoryUser: plaintext

providers:
in_memory:
memory:
users:
dunglas: { password: foo, roles: [ROLE_USER] }

firewalls:
main:
pattern: ^/
access_token:
token_handler:
cas:
validation_url: 'https://www.example.com/cas/serviceValidate'
http_client: 'Symfony\Contracts\HttpClient\HttpClientInterface'
token_extractors:
- security.access_token_extractor.cas

access_control:
- { path: ^/foo, roles: ROLE_USER }

services:
_defaults:
public: true

security.access_token_extractor.cas:
class: Symfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor
arguments:
- 'ticket'

Symfony\Contracts\HttpClient\HttpClientInterface: ~
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Http\AccessToken\Cas;

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* @see https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-V2-Specification.html
*
* @author Nicolas Attard <contact@nicolasattard.fr>
*/
final class Cas2Handler implements AccessTokenHandlerInterface
{
public function __construct(
private readonly RequestStack $requestStack,
private readonly string $validationUrl,
private readonly string $prefix = 'cas',
private ?HttpClientInterface $client = null,
) {
if (null === $client) {
if (!class_exists(HttpClient::class)) {
throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
}

$this->client = HttpClient::create();
}
}

/**
* @throws AuthenticationException
*/
public function getUserBadgeFrom(string $accessToken): UserBadge
{
$response = $this->client->request('GET', $this->getvalidationUrl($accessToken));

$xml = new \SimpleXMLElement($response->getContent(), 0, false, $this->prefix, true);

if (isset($xml->authenticationSuccess)) {
return new UserBadge((string) $xml->authenticationSuccess->user);
}

if (isset($xml->authenticationFailure)) {
throw new AuthenticationException('CAS Authentication Failure: '.trim((string) $xml->authenticationFailure));
}

throw new AuthenticationException('Invalid CAS response.');
}

private function getValidationUrl(string $accessToken): string
{
$request = $this->requestStack->getCurrentRequest();

if (null === $request) {
throw new \LogicException('Request should exist so it can be processed for error.');
}

$query = $request->query->all();

if (!isset($query['ticket'])) {
throw new AuthenticationException('No ticket found in request.');
}
unset($query['ticket']);
$queryString = empty($query) ? '' : '?'.http_build_query($query);

return sprintf('%s?ticket=%s&service=%s',
$this->validationUrl,
urlencode($accessToken),
urlencode($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$queryString)
);
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/Http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* `UserValueResolver` no longer implements `ArgumentValueResolverInterface`
* Add CAS 2.0 access token handler

6.3
---
Expand Down
Loading

0 comments on commit f1d7218

Please sign in to comment.