Unify SSO with platform for seamless sign-in across Ladill apps.
Deploy Ladill Servers / deploy (push) Successful in 24s

Add silent OAuth, deep-link guest redirects, and a session keepalive iframe.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-07 06:33:16 +00:00
co-authored by Cursor
parent 05b19ee86b
commit ebd58db9d1
5 changed files with 64 additions and 14 deletions
@@ -19,19 +19,21 @@ class SsoLoginController extends Controller
{
public function connect(Request $request): RedirectResponse
{
$intended = (string) $request->query('redirect', route('servers.dashboard'));
if (Auth::check()) {
return redirect()->route('servers.dashboard');
return $this->safeRedirect($intended, route('servers.dashboard'));
}
$verifier = Str::random(64);
$state = Str::random(40);
$request->session()->put('sso.verifier', $verifier);
$request->session()->put('sso.state', $state);
$request->session()->put('sso.intended', $request->query('redirect', route('servers.dashboard')));
$request->session()->put('sso.intended', $intended);
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$query = http_build_query([
$query = [
'response_type' => 'code',
'client_id' => (string) config('services.ladill_sso.client_id'),
'redirect_uri' => (string) config('services.ladill_sso.redirect'),
@@ -39,18 +41,40 @@ class SsoLoginController extends Controller
'state' => $state,
'code_challenge' => $challenge,
'code_challenge_method' => 'S256',
]);
];
return redirect()->away(rtrim((string) config('services.ladill_sso.issuer'), '/').'/oauth/authorize?'.$query);
if (! $request->boolean('interactive')) {
$query['prompt'] = 'none';
}
return redirect()->away(rtrim((string) config('services.ladill_sso.issuer'), '/').'/oauth/authorize?'.http_build_query($query));
}
public function callback(Request $request): RedirectResponse
{
$authLogin = 'https://'.config('app.auth_domain').'/login';
$intended = (string) $request->session()->get('sso.intended', route('servers.dashboard'));
if ($request->filled('error') || ! $request->filled('code')
if ($request->filled('error')) {
if (in_array($request->query('error'), ['login_required', 'interaction_required', 'consent_required'], true)
&& ! $request->boolean('interactive')) {
return redirect()->route('sso.connect', [
'redirect' => $intended,
'interactive' => 1,
]);
}
return redirect()->route('sso.connect', [
'redirect' => $intended,
'interactive' => 1,
]);
}
if (! $request->filled('code')
|| $request->query('state') !== $request->session()->pull('sso.state')) {
return redirect()->away($authLogin);
return redirect()->route('sso.connect', [
'redirect' => $intended,
'interactive' => 1,
]);
}
$issuer = rtrim((string) config('services.ladill_sso.issuer'), '/');
@@ -64,12 +88,18 @@ class SsoLoginController extends Controller
'code_verifier' => (string) $request->session()->pull('sso.verifier'),
]);
if ($tokenRes->failed()) {
return redirect()->away($authLogin);
return redirect()->route('sso.connect', [
'redirect' => $intended,
'interactive' => 1,
]);
}
$claims = Http::withToken((string) $tokenRes->json('access_token'))->acceptJson()->get($issuer.'/oauth/userinfo');
if ($claims->failed() || ! $claims->json('sub')) {
return redirect()->away($authLogin);
return redirect()->route('sso.connect', [
'redirect' => $intended,
'interactive' => 1,
]);
}
$user = User::updateOrCreate(
@@ -81,14 +111,13 @@ class SsoLoginController extends Controller
],
);
// Accept any pending team invites for this email on first sign-in.
\App\Models\HostingTeamMember::linkPendingInvitesFor($user);
Auth::login($user, remember: true);
$request->session()->regenerate();
$request->session()->forget('mailbox_link_banner_dismissed');
return redirect()->intended($request->session()->pull('sso.intended', route('servers.dashboard')));
return $this->safeRedirect($intended, route('servers.dashboard'));
}
public function logout(Request $request): RedirectResponse
@@ -111,8 +140,6 @@ class SsoLoginController extends Controller
$request->session()->regenerateToken();
}
// SLO chain: if the central sequencer passed a (ladill.com) return URL,
// continue the top-level redirect chain to the next app; else acknowledge.
$return = (string) $request->query('return', '');
$root = (string) config('app.platform_domain', 'ladill.com');
$host = parse_url($return, PHP_URL_HOST);
@@ -137,4 +164,17 @@ class SsoLoginController extends Controller
return strtolower((string) $user->email) !== $mailbox;
}
private function safeRedirect(string $url, string $fallback): RedirectResponse
{
$host = parse_url($url, PHP_URL_HOST);
$root = (string) config('app.platform_domain', 'ladill.com');
if (is_string($host) && str_starts_with($url, 'https://')
&& ($host === $root || str_ends_with($host, '.'.$root))) {
return redirect()->away($url);
}
return redirect()->away($fallback);
}
}