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

Cas Bundle Laravel Package

l3/cas-bundle

l3/cas-bundle integrates CAS (Central Authentication Service) single sign-on into Symfony applications. It lets users authenticate against an existing CAS server, handles the CAS login and logout flow, and configures easily so centralized credentials can secure your Symfony app with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: l3/cas-bundle is a Symfony bundle, not a Laravel package. It ships a compiler pass, a DI extension, and service definitions wired into Symfony's security firewall (l3.cas.entry_point, l3.cas.user_provider, l3.cas.logout_handler). Installing it in Laravel gets you nothing — package discovery will not run, no service provider exists, and the classes are bound to Symfony\Component\Security\* interfaces you cannot meaningfully consume.

What you actually want from this repo is the thing it wraps: apereo/phpcas. The bundle's real value as a reference is its config surface and its authentication flow, which you can port in an hour.

Minimal steps (Laravel path)

composer require apereo/phpcas

Publish a config/cas.php:

return [
    'host'        => env('CAS_HOST', 'cas.example.edu'),
    'port'        => (int) env('CAS_PORT', 443),
    'context'     => env('CAS_CONTEXT', '/cas'),
    'protocol'    => env('CAS_PROTOCOL', '2.0'),
    'service_base' => env('APP_URL'),           // must match CAS registry exactly
    'ca_cert'     => env('CAS_CA_CERT_PATH'),   // null = use system store
    'debug'       => env('CAS_DEBUG', false),
];

Wrap the static client in a singleton so it is initialised once, per request, after the session is available:

// app/Services/CasClient.php
final class CasClient
{
    private bool $booted = false;

    public function boot(): void
    {
        if ($this->booted) return;
        $c = config('cas');

        phpCAS::setLogger();                       // or setLogger(new \Psr\Log\...)
        phpCAS::setVerbose($c['debug']);
        if ($c['debug']) phpCAS::setDebug(storage_path('logs/phpcas.log'));

        phpCAS::client(
            $c['protocol'] === '2.0' ? CAS_VERSION_2_0 : CAS_VERSION_1_0,
            $c['host'], $c['port'], $c['context'], $c['service_base']
        );

        $c['ca_cert']
            ? phpCAS::setCasServerCACert($c['ca_cert'])
            : phpCAS::setCasServerCACert(ini_get('openssl.cafile') ?: '/etc/ssl/certs/ca-certificates.crt');

        $this->booted = true;
    }
}

First use case: protect a single route group with phpCAS::forceAuthentication(), read phpCAS::getUser(), and Auth::login() a locally-resolved User model. Do not try to make a distributed-CAS user be your Eloquent user — map it.

Look at the bundle's DependencyInjection/Configuration.php and Security/CasListener.php for the flow ordering (entry point → ticket validation → user provider → token). That ordering is the spec you are re-implementing.

Implementation Patterns

1. Middleware as the authentication entry point

The bundle does entry-point + listener + provider as three Symfony services. In Laravel, collapse them into one middleware — the equivalent of the bundle's CasListener after forceAuthentication() succeeds.

// app/Http/Middleware/CasAuthenticate.php
public function handle($request, Closure $next)
{
    $cas = app(CasClient::class);
    $cas->boot();

    if (! phpCAS::isAuthenticated()) {
        if (phpCAS::checkAuthentication()) {
            // ticket was just validated
        } else {
            phpCAS::forceAuthentication(); // redirects, never returns
        }
    }

    $user = Auth::user() ?? $this->resolveUser(phpCAS::getUser(), phpCAS::getAttributes());
    Auth::login($user, remember: false);
    $request->session()->put('cas_authenticated_at', now());

    return $next($request);
}

Register it as the auth alias replacement for CAS-only routes, or as a separate cas.auth alias layered in front of auth. The latter is cleaner — auth then just checks Auth::check().

2. Attribute mapping as a dedicated, testable class

The bundle's user provider reads CAS attributes and produces roles. Mirror that with an explicit mapper so it is unit-testable without phpCAS static state.

class CasAttributeMapper
{
    public function __construct(private array $map) {} // config('cas.attributes')

    public function toUser(string $username, array $attributes): User
    {
        return User::updateOrCreate(
            ['cas_username' => $username],
            [
                'name'  => $attributes[$this->map['name']]  ?? $username,
                'email' => $attributes[$this->map['email']] ?? null,
            ]
        );
    }
}

Attribute names differ wildly by CAS server (Apereo uid/mail/memberOf, Shibboleth-style urn:oid:*, legacy sAMAccountName). Keep the map in config, not in code — this is the single most common source of "works in staging, breaks at the partner university" tickets.

3. Guards for API / mixed-auth apps

If part of the app is token-authenticated (Sanctum) and part is browser CAS, don't run the CAS middleware globally. Use a custom guard so auth:cas is selectable:

Auth::viaRequest('cas', fn ($request) => Auth::getUser() ?? $this->fromCasSession($request));

Then ->middleware('auth:cas') on browser routes and auth:sanctum on API routes.

4. Logout — local and single sign-out are different operations

The bundle's logout_handler calls phpCAS logout. Decide explicitly:

Route::post('/logout', function (Request $request) {
    $cas = app(CasClient::class); $cas->boot();

    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();

    if (config('cas.single_logout')) {
        phpCAS::logoutWithRedirectService(url('/goodbye')); // kills CAS SSO everywhere
    }
    return redirect('/goodbye');
});

phpCAS::logout() in 1.3+ redirects to the CAS server's logout. That logs the user out of every CAS-protected app on the estate — users perceive this as a bug. Gate it behind config and label the button "Sign out of all applications".

5. Local development and testing

The killer problem with phpCAS is its global static state: it can only be initialised once per process and it touches $_SESSION directly. Bind an interface and fake it:

interface CasContract {
    public function user(): ?string;
    public function attributes(): array;
    public function requireLogin(): void;
}

PhpCasAdapter implements CasContract; FakeCas implements CasContract in tests and in APP_ENV=local with a CAS_FAKE_USER env var. This mirrors what the Symfony bundle gives you for free (a user_provider you can swap in security.yaml) — in Laravel you have to build it.

6. Service tickets for downstream calls

If you need to call CAS-protected backends, phpCAS::getServiceTicket() / retrievePT requires proxy configuration (allowProxyChain, proxy callback URL). The bundle exposes this via config; in Laravel add 'proxy' => [...] to config/cas.php and guard the whole block behind an enabled flag — proxy callbacks are a security surface and should be off by default.

Gotchas and Tips

  • This package will not install cleanly into modern Laravel. It caps at Symfony 5 components and has an unasserted license (NOASSERTION). Treat the repository as documentation, not a dependency. Pull apereo/phpcas only.
  • $_SESSION collision is the #1 failure. phpCAS calls session_start() internally. If Laravel's session middleware has already started a session, or vice versa, you get silent ticket-validation loops, "authentication failed" with no error, or CSRF breakage. Boot phpCAS after Laravel's session is available and before anything reads auth. Debug by checking session_id() before and after phpCAS::client().
  • The service URL must match character-for-character. Trailing slash, http vs https, www., port, and query ordering all matter. Behind a load balancer, APP_URL must be the external URL — configure TrustProxies correctly or you will send http://internal-host/... and get INVALID_SERVICE. Set phpCAS::setFixedServiceURL() if you need to override the auto-detected one.
  • Never ship phpCAS::setNoCasServerValidation(). It disables TLS verification of the CAS server and turns your SSO into a MITM target. Use setCasServerCACert(). If you must run without it locally, tie it to app()->isLocal() and log a warning at boot.
  • CAS 1.0 has no attributes. If you need role/email claims, you need CAS_VERSION_2_0 (or SAML 1.1). The bundle's protocol config drives this; get it wrong and getAttributes() returns [] with no error.
  • Enable debug logging early. phpCAS::setDebug('/path/to/phpcas.log') plus setVerbose(true) writes the raw XML exchange. Half the "it just redirects forever" tickets are solved by reading that log — you will see the cas:authenticationFailure code (INVALID_TICKET, INVALID_SERVICE, INVALID_PROXY_CALLBACK) directly.
  • Test with a real CAS server, not a mock. Ticket validation is a server round-trip with protocol subtleties (ticket reuse, clock skew). Stand up apereo/cas in Docker for integration tests; mock only at the CasContract boundary for unit tests.
  • gateway=true is the passive mode you want for "optional" login. phpCAS::checkAuthentication() uses it. Use it on marketing pages to show "Signed in as X" without forcing a redirect, then forceAuthentication() at the actual protected action.
  • Renew vs. force. forceAuthentication() accepts an existing CAS SSO session; if your security model needs credentials re-entered for sensitive actions, you need renew=true, which most phpCAS wrappers do not expose cleanly. Verify before promising step-up auth.
  • Extension point in the bundle worth copying: l3.cas.user_provider is swappable. Do the same — bind CasUserResolver to an interface and let apps override the "how do I turn a CAS identity into a local user" step. That is the only piece that is genuinely application-specific; everything else is protocol plumbing you should not re-implement per project.
  • Watch the maintenance signal. Last release 2023-08-06, zero dependents, unasserted license. Treat any code copied from it as a starting point to review, not a vetted baseline, and expect no upstream fixes.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle