subfission/cas
Laravel package for CAS (Central Authentication Service) single sign-on. Provides CAS authentication, middleware, user retrieval, and logout integration for Laravel apps, making it easy to add institutional SSO support.
Install the wrapper
composer require subfission/cas
On Laravel 5.5+ the Subfission\Cas\CasServiceProvider and Cas facade are auto-discovered. On older versions, add the provider and 'Cas' => Subfission\Cas\Facades\Cas::class to config/app.php.
Publish and fill the config
php artisan vendor:publish --provider="Subfission\Cas\CasServiceProvider"
config/cas.php maps almost 1:1 onto phpCAS options — the ones that matter first:
'hostname' => env('CAS_HOSTNAME', 'cas.example.edu'),
'port' => env('CAS_PORT', 443),
'context' => env('CAS_CONTEXT', '/cas'),
'cas_version' => env('CAS_VERSION', '2.0'), // CAS_VERSION_2_0 / CAS_VERSION_3_0
'certificate' => env('CAS_CERTIFICATE', ''), // path to CA/chain used to verify the CAS server
The service URL that phpCAS sends to the CAS server is derived from the incoming request, so APP_URL and your proxy/trusted-proxy config matter as much as the CAS host settings.
Register the middleware aliases (Laravel ≤10 in app/Http/Kernel.php, Laravel 11+ in bootstrap/app.php):
'cas.auth' => \Subfission\Cas\Middleware\Authenticate::class,
'cas.guest' => \Subfission\Cas\Middleware\RedirectIfAuthenticated::class,
First use case — protect a dashboard and dump what the CAS server actually returned:
Route::middleware('cas.auth')->group(function () {
Route::get('/dashboard', function () {
// Cas::user() is the CAS principal (username string)
// Cas::getAttributes() is the raw attribute array from the CAS response
dd(Cas::user(), Cas::getAttributes());
});
});
Once you can see the attribute names your CAS server releases (uid, mail, displayName, memberOf, …), you know exactly what you can key your local user records on.
Wrap the protected area, not individual routes. Typical shape:
Route::middleware('cas.guest')->get('/login', fn () => view('login'));
Route::middleware('cas.auth')->group(function () {
Route::get('/', [DashboardController::class, 'index']);
Route::get('/profile', [ProfileController::class, 'show']);
});
cas.auth calls Cas::check() and, if there is no valid CAS session, triggers Cas::authenticate() — a redirect to the CAS login page with a ?ticket= round-trip handled transparently on the way back. cas.guest is the inverse: already-authenticated users get bounced away from your login page.
Bridge CAS identity to a Laravel user. This is the single most important pattern: subfission/cas is not a Laravel guard/provider, so Auth::user() stays null until you log someone in yourself.
// app/Http/Middleware/SyncCasUser.php
public function handle($request, Closure $next)
{
if (Cas::check() && ! Auth::check()) {
$user = User::firstOrCreate(
['cas_uid' => Cas::user()],
[
'name' => Cas::getAttribute('displayName') ?? Cas::user(),
'email' => Cas::getAttribute('mail'),
]
);
Auth::login($user, remember: true);
}
return $next($request);
}
Register it after cas.auth so the CAS session already exists, and run it on every request (or in a Login event listener) so attribute changes on the CAS side propagate. Keeping a local users row is also how you attach roles, relations, and audit data — CAS only gives you identity, not authorization.
Read attributes defensively. Servers differ wildly in what they release, and attributes for an SSO session validated earlier may be missing if the release policy changed. Prefer:
Cas::hasAttribute('mail') ? Cas::getAttribute('mail') : null;
// and always keep a fallback to Cas::user()
Logout. CAS logout is global single logout — it invalidates the SSO session across every application on that CAS server. Do local cleanup first, then hand off:
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->away(Cas::logout()); // or Cas::logout() if it returns a redirect
}
Consider a confirmation page: users rarely expect "log out of this app" to log them out of their email and intranet too.
Single sign-on across apps comes for free: the second Laravel app on the same CAS server hits its cas.auth route, Cas::check() finds an existing CAS session cookie, and the user never sees a login form. Test this explicitly — it's the whole point of CAS and the thing most likely to break if your service URLs are inconsistent.
Testing. Because the session is the source of truth, feature tests can stub the CAS session instead of doing redirect dances — set the CAS session keys the package uses after login, or bind a mock Cas instance into the container for middleware tests. Reserve real browser tests (Dusk) for the actual redirect/ticket flow.
Cas::user() ≠ Auth::user(). The most common first bug report. The package stores the CAS principal in the session and exposes it through the facade; Laravel's auth system is untouched. You must call Auth::login() yourself (see above).http:// or an internal hostname, and the CAS server rejects the ticket (INVALID_TICKET / "service not authorized"). Fix with correct TrustProxies configuration and APP_URL, not by hacking the config.certificate must point at the CA/chain that signed the CAS server cert. Empty or wrong values produce opaque phpCAS failures. Use the debug flag in the config (and phpCAS's debug output) while bringing a new environment up — then turn it off.cas_user between requests, "headers already sent" warnings, or a login loop. Check that CAS middleware runs inside the web group and that your session driver behaves under the redirect round-trip.Cas::check() never returns true after the redirect — bad service URL, expired/replayed ticket, or a session that isn't persisting cookies. Log the return URL and ticket parameter on the callback route.?ticket=. Don't put CAS-protected URLs behind aggressive full-page caching.cas.auth must appear before any middleware that assumes Auth::check(). If you add your SyncCasUser middleware, it must come after cas.auth in the group.Cas::check() / Cas::authenticate() are request/redirect-oriented and depend on session and HTTP context. In jobs, look up the local User by cas_uid instead.php artisan config:cache after changing CAS settings; stale cached config is a classic "I changed the hostname and nothing happened."Cas:: calls behind one service class), and plan an exit: either depend on apereo/phpcas directly inside a custom middleware, or implement a proper Laravel guard so the swap is localized.CasAuthenticator/CasUserResolver class rather than sprinkling Cas:: calls through controllers. When you migrate off the archived package — or need a second identity provider — you change one class instead of forty.How can I help you explore Laravel packages today?