Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

Hi,

On this page, it is indicated that "Symfony 7.1 introduces a new CAS 2.0 access token handler".
I can't implement it.

I'm using Symfony 7.1.1 :

bin/console --version
Symfony 7.1.1 (env: dev, debug: true)

security.yaml :

security:
    role_hierarchy:
        ROLE_ADMIN: [ ROLE_USER ]
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: username
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            provider: app_user_provider
            access_token:
                token_handler:
                    cas:
                        validation_url: https://auth.domain.tld/cas/validate

    access_control:
        - { path: ^/, roles: ROLE_ADMIN }

https://auth.domain.ltd/cas/validate is actually the real validation URL of my CAS server.

Result is code 401, I'm not redirected to the authentication form of CAS server :

image

My App\Entity\User entity was just generated with bin/console make:user.
If I disable access control rule, I see the controller response so route is working well.

Maybe I forgot something ?
Can anybody help me ?

You must be logged in to vote

Replies: 12 comments · 3 replies

Comment options

Hello,

never used the cas token handler personally, but from your description it seems you’re simply missing an entry point?

You must be logged in to vote
0 replies
Comment options

Hello,

Any new?
I'm also trying to use the new CAS 2.0 access token handler. But I haven't found enough examples!

You must be logged in to vote
0 replies
Comment options

I have the same issue. It seems this tokenAuthentication needs more documentation to be used, and perhaps more code to allow easy usage for developers.

You must be logged in to vote
0 replies
Comment options

I got the CAS access token working. Not sure it's the intended usage from the original author.
As @MatTheCat commented, the entry point is mandatory: You must define a service implementing AuthenticationEntryPointInterface

Moreover, the configuration is missing also a snippet:

                token_extractors:
                    - security.access_token_extractor.cas

You can find a working demo in my repo (adapt user from users_in_memories, and the CAS URL in the .env):

https://github.com/moobyfr/symfony7-demo-authcas

You must be logged in to vote
0 replies
Comment options

Thank you very much @moobyfr for this comprehensive example.

I previously solved my one problem with a custom mixed authentication based on phpCAS.

But your solution is the simplest that can be written for a CAS only authentication,
and should be part of the official documentation.

The symfony example propose two routes:

  • "/" which en the public page and display a "Lucky Number" link
  • "/number" the "Lucky Number" page with CAS protection which display a random number

For those who want to try it out, here are the steps.

  1. Download and install the demo from the @moobyfr previous reply
composer install
  1. Update the two server urls in the .env file
CAS_URL="https://cas.domain.tld/cas/"
CAS_VALIDATE_URL="https://cas.domain.tld/cas/serviceValidate"

Il my case, I only had to update the two cas.domain.tld/cas pathes.

  1. Add existing user in config/packages/security.yaml

Then add a new existing login (something jnown bu the CAS server)
in the users_in_memory provider.
(Just duplicate and update the foobar login line).

  1. test with internal Symfony server
symfony serve

But phpCAS also allows you to obtain additional attributes from the CAS server
(for example, firstname, lastname and email) that I don't know how to get
from the symfony server.

You must be logged in to vote
0 replies
Comment options

My demo code isn't complete, the logout route is mandatory for easyadmin for example , I have a basic fix for it.

The main problem is a 'while page' problem: when the service URL is redirected to another URL with 302, the authentication is broken.
I'm not even sure if this implementation of the token auth for CAS is usable.

The problem had also happened with my demo application, but I wasn't sure to be able to reproduce it.

  • Initial access to https://localhost/admin
  • Redirection to https://cas/login?service=https://localhost/admin
  • auth cas done
  • Redirection to https://localhost/admin?ticket=ST... (Auth is OK)
  • easyadmin redirects with a 302 to /admin?crudAction=index...&ticket=ST-1 -> 401 response

if I access again to the url without the ticket, all is ok

[Web Server ] Aug  9 19:53:58 |INFO   | SERVER GET  (302) /admin ip="127.0.0.1"
[Application] Aug  9 16:53:58 |INFO   | REQUES Matched route "admin". method="GET" request_uri="https://localhost:8000/admin" route="admin" route_parameters={"_controller":"App\\Controller\\Admin\\DashboardController::index","_route":"admin"}
[Application] Aug  9 16:53:58 |DEBUG  | SECURI Checking for authenticator support. authenticators=1 firewall_name="main"
[Application] Aug  9 16:53:58 |DEBUG  | SECURI Checking support on authenticator. authenticator="Symfony\\Component\\Security\\Http\\Authenticator\\AccessTokenAuthenticator"
[Application] Aug  9 16:53:58 |DEBUG  | SECURI Authenticator does not support the request.
[Application] Aug  9 16:53:58 |DEBUG  | SECURI Access denied, the user is not fully authenticated; redirecting to authentication entry point.
[Application] Aug  9 16:53:58 |DEBUG  | SECURI Calling Authentication entry point. entry_point={"App\\Security\\CasAuthenticatorEntryPoint":[]}
[Application] Aug  9 16:54:15 |INFO   | REQUES Matched route "admin". method="GET" request_uri="https://localhost:8000/admin?ticket=ST-165605-iYmOdJG-aJNraX1Mm82xDbx-qOo-ffb331732889" route="admin" route_parameters={"_controller":"App\\Controller\\Admin\\DashboardController::index","_route":"admin"}
[Application] Aug  9 16:54:15 |DEBUG  | SECURI Checking for authenticator support. authenticators=1 firewall_name="main"
[Application] Aug  9 16:54:15 |DEBUG  | SECURI Checking support on authenticator. authenticator="Symfony\\Component\\Security\\Http\\Authenticator\\AccessTokenAuthenticator"
[Web Server ] Aug  9 19:54:15 |INFO   | SERVER GET  (302) /admin?ticket=ST-165605-iYmOdJG-aJNraX1Mm82xDbx-qOo-ffb331732889 ip="127.0.0.1"
[Application] Aug  9 16:54:15 |INFO   | SECURI Authenticator successful! authenticator="Symfony\\Component\\Security\\Http\\Authenticator\\AccessTokenAuthenticator" token={"Symfony\\Component\\Security\\Http\\Authenticator\\Token\\PostAuthenticationToken":"PostAuthenticationToken(user=\"test.blindauer\", roles=\"ROLE_USER\")"}
[Application] Aug  9 16:54:15 |DEBUG  | SECURI Authenticator set no success response: request continues.
[Application] Aug  9 16:54:15 |DEBUG  | SECURI Stored the security token in the session. key="_security_main"
[Application] Aug  9 16:54:15 |INFO   | REQUES Matched route "admin". method="GET" request_uri="https://localhost:8000/admin?crudAction=index&crudControllerFqcn=App%5CController%5CAdmin%5CEventCrudController&ticket=ST-165605-iYmOdJG-aJNraX1Mm82xDbx-qOo-ffb331732889" route="admin" route_parameters={"_controller":"App\\Controller\\Admin\\DashboardController::index","_route":"admin"}
[Application] Aug  9 16:54:15 |DEBUG  | SECURI Read existing security token from the session. key="_security_main" token_class="Symfony\\Component\\Security\\Http\\Authenticator\\Token\\PostAuthenticationToken"
[Application] Aug  9 16:54:15 |DEBUG  | SECURI User was reloaded from a user provider. provider="Symfony\\Component\\Security\\Core\\User\\InMemoryUserProvider" username="test.blindauer"
[Application] Aug  9 16:54:15 |DEBUG  | SECURI Checking for authenticator support. authenticators=1 firewall_name="main"
[Application] Aug  9 16:54:15 |DEBUG  | SECURI Checking support on authenticator. authenticator="Symfony\\Component\\Security\\Http\\Authenticator\\AccessTokenAuthenticator"
[Web Server ] Aug  9 19:54:16 |WARN   | SERVER GET  (401) /admin?crudAction=index&crudControllerFqcn=App%5CController%5CAdmin%5CEventCrudController&ticket=ST-165605-iYmOdJG-aJNraX1Mm82xDbx-qOo-ffb331732889 ip="127.0.0.1"
[Application] Aug  9 16:54:16 |INFO   | SECURI Authenticator failed. authenticator="Symfony\\Component\\Security\\Http\\Authenticator\\AccessTokenAuthenticator"
[Application] Aug  9 16:54:16 |DEBUG  | SECURI The "Symfony\Component\Security\Http\Authenticator\AccessTokenAuthenticator" authenticator set the failure response.
[Application] Aug  9 16:54:16 |DEBUG  | SECURI The "Symfony\Component\Security\Http\Authenticator\AccessTokenAuthenticator" authenticator set the response. Any later authenticator will not be called
[Application] Aug  9 16:54:16 |DEBUG  | SECURI Stored the security token in the session. key="_security_main"
You must be logged in to vote
0 replies
Comment options

Hi,

Thank you all, I'll try this soon !

You must be logged in to vote
0 replies
Comment options

Well, thank you, I finally managed to implement native CAS authentication of Symfony 7.1.

.env :

###> cas ###
CAS_URL="https://auth.domain.tld/cas"
CAS_VALIDATE_PATH="/serviceValidate"
CAS_LOGIN_PATH="/login"
###< cas ###

.env.local :

###> cas ###
CAS_URL="https://my.domain.com/cas"
###< cas ###

services.yaml :

parameters:
    cas_url: '%env(CAS_URL)%'
    cas_validate_url: '%cas_url%%env(CAS_VALIDATE_PATH)%'
    cas_login_url: '%cas_url%%env(CAS_LOGIN_PATH)%'
services:
    security.access_token_extractor.cas:
        class: Symfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor
        arguments:
            - 'ticket'
    App\Security\CasAuthenticatorEntryPoint:
        arguments:
            $casLoginUrl: '%cas_login_url%'

security.yaml :

security:
    firewalls:
        main:
            pattern: ^/
            provider: app_user_provider
            access_token:
                token_handler:
                    cas:
                        validation_url: '%cas_validate_url%'
                token_extractors:
                    - security.access_token_extractor.cas
            entry_point: App\Security\CasAuthenticatorEntryPoint

CasAuthenticatorEntryPoint.php :

<?php

namespace App\Security;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

class CasAuthenticatorEntryPoint implements AuthenticationEntryPointInterface
{
    private string $casLoginUrl;

    public function __construct(string $casLoginUrl)
    {
        $this->casLoginUrl = $casLoginUrl;
    }

    public function start(Request $request, AuthenticationException $authException = null): Response
    {
        $redirectUrl = sprintf('%s?service=%s', $this->casLoginUrl, urlencode($request->getUri()));

        return new RedirectResponse($redirectUrl);
    }
}

Authentication works well but once logged in, the CAS ticket is retained in the URI.
To clean the URI I use this workaroud :

CasAuthenticatorListener.php :

<?php

namespace App\Security;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;

#[AsEventListener(event: LoginSuccessEvent::class, method: 'onLoginSuccessEvent')]
final class CasAuthenticatorListener
{
    public function onLoginSuccessEvent(LoginSuccessEvent $event): void
    {
        $request     = $event->getRequest();
        $scheme      = $request->getScheme();
        $port        = $request->getPort();
        $queryParams = $request->query->all();

        $portString = '';
        if (
            ($scheme === 'http'  && $port !== 80) ||
            ($scheme === 'https' && $port !== 443)
        ) {
            $portString = ':'.$port;
        }

        unset($queryParams['ticket']);
        $queryString = count($queryParams) ? '?'.http_build_query($queryParams) : '';
        
        $uri = sprintf(
            '%s://%s%s%s%s',
            $scheme,
            $request->getHost(),
            $portString,
            $request->getPathInfo(),
            $queryString
        );
        
        $event->setResponse(new RedirectResponse($uri));
    }
}

Thanks again !

You must be logged in to vote
1 reply
@Collectonian
Comment options

Thank you! Been struggling with doing a more pure Symfony CAS login versus having to integrate PHPCAS and your answer finally helped me get it working. Really hope they fix the documentation!

Comment options

Simplified version of CasAuthenticatorListener.php :

<?php

namespace App\Security;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;

#[AsEventListener(event: LoginSuccessEvent::class, method: 'onLoginSuccessEvent')]
final class CasAuthenticatorListener
{
    public function onLoginSuccessEvent(LoginSuccessEvent $event): void
    {
        $request = $event->getRequest();
        
        $request->query->remove('ticket');
        $request->overrideGlobals();

        $event->setResponse(new RedirectResponse($request->getUri()));
    }
}
You must be logged in to vote
0 replies
Comment options

I've updated the demo application with your code, thank you!

You must be logged in to vote
0 replies
Comment options

As @diam said, CAS also allows you to obtain additional attributes from the CAS server (for example, firstname, lastname, email, etc.).
My CAS server is configured to pass some attributes but I don't know how to get them.
I've tried methods getAttribute()/getAttributes() of token class from CasAuthenticatorListener.php :

$event->getAuthenticatedToken()->getAttributes()

This array is empty.
Does anyone know how to do it ?

You must be logged in to vote
0 replies
Comment options

- reply to myself -

I successfully retrieve additional attributes from the CAS server with this patch :

vendor/symfony/security-http/AccessToken/Cas/Cas2Handler.php :

    /**
     * @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)) {
            // START PATCH
            // - before -
            // return new UserBadge((string) $xml->authenticationSuccess->user);
            // - after -
            $attributes = isset($xml->authenticationSuccess->attributes) ? (array) $xml->authenticationSuccess->attributes : [];
            return new UserBadge((string) $xml->authenticationSuccess->user, null, $attributes);
            // END PATCH
        }

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

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

We can now get attributes from listener :
CasAuthenticatorListener.php :

[...]
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
[...]
public function onLoginSuccessEvent(LoginSuccessEvent $event): void
{
    $attributes = $event->getPassport()->getBadge(UserBadge::class)->getAttributes();
    [...]
}

But I don't know if the attributes property of UserBadge class is supposed to contain the CAS attributes...

You must be logged in to vote
2 replies
@nextpageblog
Comment options

Hello @benjamin-feron. For your information, a pull request similar to your solution is open.

@benjamin-feron
Comment options

Thank you @nxtpge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
6 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.