korotovsky/sso-library
korotovsky/sso-library is a Laravel package for implementing single sign-on across multiple applications. It lets users authenticate once and access connected Laravel apps, simplifying shared login flows and centralizing authentication.
Read the framing before the code: the two descriptions disagree. The package blurb calls it "Laravel/PHP," but the repository is a Symfony2 bundle (a Bundle class, a DependencyInjection extension, and a security.firewalls.*.sso factory). There is no composer require path into a modern Laravel app — Symfony2-era constraints target PHP 5.x/7.0 and will fail platform checks on 8.2+. Treat install as a non-goal on day one.
Where to look first, in this order:
DependencyInjection/ and the security firewall factory — this is where the broker URL, client credentials, and callback route are wired. That file is effectively the configuration reference; there is no Laravel-style config/sso.php because Laravel isn't the target.The first use case: a scratch Laravel 11 app on PHP 8.2 where you hand-roll the flow the package describes — a GET /auth/sso/redirect that bounces the browser to the broker's login with a callback URL, a GET /auth/sso/callback that takes the returned ticket, validates it server-side against the broker, resolves a local User, and calls Auth::login($user, true). Build that against a fake broker (a route in the same app that mints tickets) before touching the real library. If your callback works against a fake broker and the package's code confirms the same wire format, you have what you need. Everything after that is your code, not theirs.
Write the wire-format note before you write the adapter. Endpoints, HTTP verbs, query/body parameter names, ticket format and lifetime, whether validation is back-channel or signature-based, cookie names, and the error codes. That note is the deliverable; the ported PHP is disposable.
Extract the protocol core, delete the Symfony plumbing. Copy the ticket-validation and response-parsing classes into app/Support/Sso (or a private package). Strip Symfony\Component\HttpFoundation\Request, Session, and Container references and replace their arguments with plain scalars, a PSR-7 request, or a small SsoResponse value object. Never shim Symfony's request object into Laravel — it drags the whole HTTP kernel in behind it.
Prefer middleware + Auth::login() over a custom guard. A sso.auth guard via Auth::extend() sounds idiomatic but fights you: SSO here is a redirect round-trip, not a stateless credential check, so a guard ends up holding request state it can't see. The cleaner shape is two routes plus one middleware:
Route::get('/auth/sso/redirect', [SsoController::class, 'redirect'])->name('sso.redirect');
Route::get('/auth/sso/callback', [SsoController::class, 'callback'])->name('sso.callback');
with the callback doing validate → resolve → Auth::login($user, true) → redirect()->intended(...). Keep the middleware for protecting routes (auth plus an EnsureSsoSession that checks the broker session is still live if you support single logout).
Model identity attachment explicitly, not by email alone. Add an sso_identities table (provider, subject, user_id, unique on provider+subject) rather than looking users up by email every time. Email is mutable at the IdP and is a weak join key; the subject/identifier is not. The package's "configurable user attachment" is a closure that receives the SSO payload and returns a User — mirror that as a config-bound resolver so tests and multi-tenant installs can swap it:
// config/sso.php
'resolve_user' => fn (SsoIdentity $id) => User::firstOrCreate(
['email' => $id->email],
['name' => $id->name, 'password' => Str::random(64)]
),
Validate tickets server-side, always. The callback's ?ticket=... is an opaque pointer, never an identity. Exchange it back-channel, bind the validation to the exact callback URL you sent, and consume it once. Store used tickets in the cache with a TTL slightly longer than the broker's own:
abort_unless(Cache::add("sso:ticket:{$ticket}", true, now()->addMinutes(5)), 401);
Implement local logout and global logout as separate actions. logout forgets the Laravel session; sso.logout also invalidates at the broker and redirects. Getting this wrong produces the classic "logged out here, still logged in there" loop.
Test against a fake broker, not the real thing. A FakeBroker test double that mints valid, expired, replayed, and tampered tickets gives you the four tests that actually matter — happy path, replay rejected, expired rejected, wrong-audience rejected. Feature tests through $this->get('/auth/sso/callback?ticket=...') with RefreshDatabase will catch almost everything.
composer require to fail on PHP constraints, and --ignore-platform-reqs to get you a package that fatals on PHP 8.2 for era-typical reasons (each(), ${var} interpolation, dynamic properties, missing return types). Do not plan around making it install.catch (\Exception $e) { return true; } around validation, or a try that lets the user through on a network error to the broker. Any error in the validation path must be a 401.Cache::add guard is two lines and closes the hole.InResponseTo correlation, and signature-algorithm rejection (SHA-1 defaults were common in this era) are frequently absent. Add them in your adapter and write a test per check.NotBefore / NotOnOrAfter; zero tolerance causes intermittent failures across hosts, and generous windows are a security smell. Make it configurable.SESSION_DOMAIN, SESSION_SECURE_COOKIE, and SameSite. A SameSite=Strict session cookie will be dropped on the redirect back from the broker and produce a login loop that only reproduces on staging. Use Lax for the callback.returnTo / RelayState. Whitelist allowed hosts and paths before redirect()->intended(); the intended-URL mechanism is otherwise an attacker-controlled redirect after a successful login.env() outside config/sso.php — under php artisan config:cache it returns null and your broker URL vanishes in production only.How can I help you explore Laravel packages today?