8000 feature #48276 [Security] add CAS 2.0 AccessToken handler (nacorp) · symfony/symfony@3a4889f · GitHub
[go: up one dir, main page]

Skip to content

Commit 3a4889f

Browse files
committed
feature #48276 [Security] add CAS 2.0 AccessToken handler (nacorp)
This PR was squashed before being merged into the 7.1 branch. Discussion ---------- [Security] add CAS 2.0 AccessToken handler | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | | License | MIT | Doc PR | in progress Hello, Thanks to the new access token, I've added the [CAS](https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-V2-Specification.html) one. In order to make it work : services.yaml ```yaml Symfony\Component\Security\Http\AccessToken\Handler\CasHandler: arguments: $validationUrl: '%env(CAS_SERVER_VALIDATION_URL)%' security.access_token_extractor.cas: class: Symfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor arguments: - 'ticket' ``` Thank you `@welcoMattic` for the conference at the SymfonyCon and `@jeremyFreeAgent` for your support on my first PR on this project! Commits ------- a1be5df [Security] add CAS 2.0 AccessToken handler
2 parents db5021f + a1be5df commit 3a4889f

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;
    @@ -78,6 +79,7 @@ public function build(ContainerBuilder $container): void
    7879
    new ServiceTokenHandlerFactory(),
    7980
    new OidcUserInfoTokenHandlerFactory(),
    8081
    new OidcTokenHandlerFactory(),
    82+
    new CasTokenHandlerFactory(),
    8183
    ]));
    8284

    8385
    $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();
    @@ -218,6 +240,7 @@ private function createTokenHandlerFactories(): array
    218240
    new ServiceTokenHandlerFactory(),
    219241
    new OidcUserInfoTokenHandlerFactory(),
    220242
    new OidcTokenHandlerFactory(),
    243+
    new CasTokenHandlerFactory(),
    221244
    ];
    222245
    }
    223246
    }

    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 1241 +
    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
    @@ -18,6 +18,7 @@ CHANGELOG
    1818

    1919
    * `UserValueResolver` no longer implements `ArgumentValueResolverInterface`
    2020
    * Deprecate calling the constructor of `DefaultLoginRateLimiter` with an empty secret
    21+
    * Add CAS 2.0 access token handler
    2122

    2223
    6.3
    2324
    ---

    0 commit comments

    Comments
     (0)
    0