Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Symfony Cas Bundle Laravel Package

univlorraine/symfony-cas-bundle

Symfony bundle that adds CAS single sign-on support, integrating Central Authentication Service login with Symfony Security, configurable CAS server settings, authentication and logout handling, and user provisioning for university or enterprise applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: this is a Symfony bundle, not a Laravel package. There is no service provider, no Guard, no config/auth.php driver. You leverage it by dropping it into a Symfony app and wiring it into the Security component's authenticator system. If you're a Laravel dev inheriting a Symfony app, the mental model is: composer require → one YAML file → one firewall entry → the bundle's authenticator does the rest.

Install and pin:

composer require univlorraine/symfony-cas-bundle

Pin an exact version in composer.json. Four stars and zero dependents means you are the QA team; a floating constraint will eventually hand you a broken minor.

Where to look first, in this order:

  1. DependencyInjection/Configuration.php — the authoritative list of config keys. The README drifts from the code; this file does not.
  2. The authenticator class under Security/ — this is the thing your firewall references. Confirm its service ID before you write YAML.
  3. composer.json of the package — check which phpCAS (apereo/phpcas vs the older jasig/phpcas) it pulls, and whether it declares a PHP/Symfony range that matches your app.

Minimal config (keys vary by release — cross-check against Configuration.php):

# config/packages/cas.yaml
univ_lorraine_symfony_cas:
    host: cas.example.edu
    port: 443
    path: /cas
    protocol: https
    certificate: '%kernel.project_dir%/config/certs/cas-chain.pem'
# config/packages/security.yaml
security:
    providers:
        cas:
            id: App\Security\CasUserProvider   # your class, see below
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: cas
            custom_authenticators:
                - univ_lorraine_symfony_cas.authenticator   # verify service ID
            entry_point: univ_lorraine_symfony_cas.authenticator
            logout:
                path: /logout
                target: /

First use case to get green: protect a single route (/admin) behind the CAS firewall, print $this->getUser()->getUserIdentifier() in a controller, and confirm the round trip works against a real CAS server. Do not attempt attribute-to-entity mapping, roles, or single logout on day one — you will conflate phpCAS session problems with Symfony security problems.

Implementation Patterns

Firewall-first, not controller-first. The idiomatic shape is a firewall whose entry_point is the CAS authenticator. Every unauthenticated request to a protected path is redirected to /cas/login?service=..., and the ticket query parameter on the way back is validated by phpCAS during authenticate(). Do not hand-roll a /cas/callback route unless you have to; you'll be reimplementing ticket validation badly.

Custom authenticator as the escape hatch. The bundle's authenticator is fine for "give me the CAS uid." The moment you need email, display name, group memberships, or JIT user provisioning, write your own and keep the bundle for the client/config only:

final class CasAuthenticator extends AbstractAuthenticator
{
    public function __construct(
        private CasClient $cas,          // the bundle's service, decorate or alias it
        private CasUserProvider $users,
    ) {}

    public function supports(Request $request): bool
    {
        return true; // CAS is the only auth path on this firewall
    }

    public function authenticate(Request $request): Passport
    {
        $this->cas->forceAuthentication();     // redirects + exits when no ticket
        $uid = $this->cas->getUser();

        return new SelfValidatingPassport(
            new UserBadge($uid, fn (string $id) => $this->users->loadFromCas(
                $id,
                $this->cas->getAttributes(),   // uid, mail, cn, memberOf...
            ))
        );
    }

    public function start(Request $request, ?AuthenticationException $e = null): Response
    {
        $this->cas->forceAuthentication();
        return new Response('', Response::HTTP_UNAUTHORIZED);
    }
}

The SelfValidatingPassport is correct here: phpCAS already proved identity, so there is no password to check. Resist the urge to invent a credentials class.

Sync attributes on every login, not just on creation. CAS is the system of record for roles and email. In your user provider, update the entity from getAttributes() on each authentication and persist. Otherwise a user demoted in the directory keeps their Symfony ROLE_ADMIN forever. If persistence on every request is too expensive, do it in a LoginSuccessEvent listener and store a hash of the attributes to skip no-op writes.

Multi-firewall layouts. Typical shape in a university app:

firewalls:
    api:   { pattern: ^/api, stateless: true, custom_authenticators: [App\Security\ApiTokenAuthenticator] }
    main:  { pattern: ^/,    provider: cas, custom_authenticators: [CasAuthenticator], entry_point: CasAuthenticator }

Order matters and pattern is first-match-wins. Put stateless API firewalls above the CAS one, or the CAS redirect will hijack Accept: application/json clients. Set stateless: false on the CAS firewall — phpCAS is inherently session-backed and a stateless firewall will fight it.

Logout via CAS single logout (SLO). Local logout (clearing the Symfony session) leaves the CAS ticket-granting cookie intact, so the user is silently logged straight back in. Implement a logout listener or controller that clears the local session and redirects to https://{cas}/cas/logout?service={returnUrl}. Many campuses disable CAS SLO back-channel POSTs; front-channel redirect is the reliable path.

Testing seam. Two layers:

  • Unit/integration tests: replace the CAS client service in config/packages/test/ with a stub returning a fixed uid and attribute array, then use $client->loginUser($user) for authenticated flows. Never let tests touch a real CAS server.
  • One end-to-end test against a fake CAS (a small container serving /login and /serviceValidate) to catch the service-URL and ticket-validation bugs that unit tests structurally cannot.

Local development. Run phpCAS in its dev context (certificate validation off) only under %kernel.environment% === 'dev', and ideally point at a campus CAS test instance. Everything else — trusting proxies, cookie domains, session storage — should be identical to prod, or you will ship a bug you cannot reproduce.

Gotchas and Tips

phpCAS owns global state and the PHP session. It calls session_start() itself, writes $_SESSION['phpCAS'], and sometimes calls exit. This collides with Symfony's session handler (framework.session.storage_factory_id) and with Redis/PdoSessionHandler. Symptoms: intermittent redirect loops, lost flash messages, "headers already sent". Mitigation: let phpCAS run first in the request lifecycle (the authenticator does this naturally), keep the CAS firewall non-lazy-safe, and avoid custom session storage until you've proven the baseline works.

The service URL must match exactly. phpCAS derives the service= URL from the incoming request. Behind a load balancer or TLS terminator it will derive http:// or an internal hostname, the CAS server rejects it, and you get an infinite login loop with no error. Fix: Request::setTrustedProxies([...], Request::HEADER_X_FORWARDED_ALL) in public/index.php, plus correct trusted_hosts/router.request_context config. This is the single most common failure mode.

start() must actually redirect. A common bug is implementing start() to throw instead of calling forceAuthentication(). phpCAS is what constructs the redirect URL; throwing yields a bare 401 and the user never reaches the CAS login page.

Certificate configuration. Use setCasServerCACert() with a real chain file in prod. setNoCasServerValidation() silently disables TLS verification — audit the config for it before every deploy. The bundle's dev/prod context toggle is exactly where this can slip through.

Autoloading phpCAS. Older phpCAS distributions are not PSR-4 (CAS.php with a pile of includes). If you see Class "phpCAS" not found despite the package being installed, check whether the bundle declares it under autoload.files; if not, require it manually in config/bootstrap.php. Newer versions are fine.

Session migration eats CAS state. After successful authentication, Symfony migrates the session ID (fixation protection). Depending on ordering, the migrated session may not carry phpCAS's stored service/ticket data, causing a re-auth on the next request. If you see double redirects to CAS on first login, this is why — check security.firewalls.main.stateless and whether a custom session listener is interfering.

Debugging toolkit:

  • phpCAS::setDebug('/tmp/phpcas.log') — turn it on, reproduce once, turn it off. It's verbose but it prints the exact validation URL and XML response.
  • Tail var/log/dev.log while reproducing; the redirect loop will be visible as repeated ^/cas entries.
  • curl -v 'https://cas.example.edu/cas/serviceValidate?service=<url-encoded>&ticket=ST-...' — replayed tickets are rejected, so use a fresh one, but this confirms whether the problem is CAS-side or Symfony-side.
  • Chrome DevTools "Preserve log" — you will lose the redirect chain otherwise.

Extension points worth knowing: the CAS client is a normal Symfony service, so decorate it rather than editing the bundle; LoginSuccessEvent / LoginFailureEvent are the right hook for provisioning and audit logging; the user provider is entirely yours; a custom CasAttributeMapper (or equivalent) lets you rename mail → email without touching the firewall.

Treat this dependency as at-risk. Four stars, zero dependents, a single maintainer, a non-standard license (NOASSERTION — read LICENSE before shipping it commercially). Fork it into your own namespace, pin the fork, and keep the fake-CAS integration test. The API surface you actually use is small (config + authenticator + client), so a fork is cheap; being surprised by an abandoned upstream mid-semester is not.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log