Improve cross-app SSO when switching from QR Plus to Events.
Deploy Ladill Events / deploy (push) Successful in 26s

Keep the shared auth session alive with periodic pings, store OIDC refresh tokens for silent re-login, and route launcher links through SSO connect.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-07 12:06:13 +00:00
co-authored by Cursor
parent 2c1c3cdd3e
commit 1d5b85f774
4 changed files with 88 additions and 16 deletions
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\QrTeamMember; use App\Models\QrTeamMember;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Client\Response as HttpResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@@ -26,6 +27,10 @@ class SsoLoginController extends Controller
return $this->safeRedirect($intended, route('events.dashboard')); return $this->safeRedirect($intended, route('events.dashboard'));
} }
if ($this->attemptSilentRefresh($request, $intended)) {
return $this->safeRedirect($intended, route('events.dashboard'));
}
$verifier = Str::random(64); $verifier = Str::random(64);
$state = Str::random(40); $state = Str::random(40);
$request->session()->put('sso.verifier', $verifier); $request->session()->put('sso.verifier', $verifier);
@@ -44,6 +49,11 @@ class SsoLoginController extends Controller
'code_challenge_method' => 'S256', 'code_challenge_method' => 'S256',
]; ];
$loginHint = (string) $request->session()->get('sso.login_hint', '');
if ($loginHint !== '') {
$query['login_hint'] = $loginHint;
}
if (! $request->boolean('interactive')) { if (! $request->boolean('interactive')) {
$query['prompt'] = 'none'; $query['prompt'] = 'none';
} }
@@ -95,23 +105,14 @@ class SsoLoginController extends Controller
]); ]);
} }
$claims = Http::withToken((string) $tokenRes->json('access_token'))->acceptJson()->get($issuer.'/oauth/userinfo'); $user = $this->loginFromTokenResponse($request, $tokenRes);
if ($claims->failed() || ! $claims->json('sub')) { if (! $user) {
return redirect()->route('sso.connect', [ return redirect()->route('sso.connect', [
'redirect' => $intended, 'redirect' => $intended,
'interactive' => 1, 'interactive' => 1,
]); ]);
} }
$user = User::updateOrCreate(
['public_id' => (string) $claims->json('sub')],
[
'name' => $claims->json('name'),
'email' => $claims->json('email') ?: (string) $claims->json('sub').'@users.ladill.com',
'avatar_url' => $claims->json('picture'),
],
);
QrTeamMember::linkPendingInvitesFor($user); QrTeamMember::linkPendingInvitesFor($user);
Auth::login($user, remember: true); Auth::login($user, remember: true);
@@ -150,6 +151,68 @@ class SsoLoginController extends Controller
return response('', 204); return response('', 204);
} }
private function attemptSilentRefresh(Request $request, string $intended): bool
{
$refreshToken = (string) $request->session()->get('sso.refresh_token', '');
if ($refreshToken === '') {
return false;
}
$issuer = rtrim((string) config('services.ladill_sso.issuer'), '/');
$tokenRes = Http::asForm()->post($issuer.'/oauth/token', [
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => (string) config('services.ladill_sso.client_id'),
'client_secret' => (string) config('services.ladill_sso.client_secret'),
'scope' => 'openid profile email',
]);
if ($tokenRes->failed()) {
$request->session()->forget('sso.refresh_token');
return false;
}
$user = $this->loginFromTokenResponse($request, $tokenRes);
if (! $user) {
return false;
}
Auth::login($user, remember: true);
$request->session()->put('sso.intended', $intended);
return true;
}
private function loginFromTokenResponse(Request $request, HttpResponse $tokenRes): ?User
{
$refreshToken = (string) $tokenRes->json('refresh_token', '');
if ($refreshToken !== '') {
$request->session()->put('sso.refresh_token', $refreshToken);
}
$issuer = rtrim((string) config('services.ladill_sso.issuer'), '/');
$claims = Http::withToken((string) $tokenRes->json('access_token'))->acceptJson()->get($issuer.'/oauth/userinfo');
if ($claims->failed() || ! $claims->json('sub')) {
return null;
}
$email = (string) ($claims->json('email') ?: '');
if ($email !== '') {
$request->session()->put('sso.login_hint', $email);
}
return User::updateOrCreate(
['public_id' => (string) $claims->json('sub')],
[
'name' => $claims->json('name'),
'email' => $email !== '' ? $email : (string) $claims->json('sub').'@users.ladill.com',
'avatar_url' => $claims->json('picture'),
],
);
}
private function shouldLogoutForMailbox(Request $request): bool private function shouldLogoutForMailbox(Request $request): bool
{ {
$mailbox = strtolower(trim((string) $request->query('mailbox', ''))); $mailbox = strtolower(trim((string) $request->query('mailbox', '')));
+2 -2
View File
@@ -30,7 +30,7 @@ return [
['name' => 'Domains', 'url' => 'https://domains.'.$root, 'icon' => 'domains.svg'], ['name' => 'Domains', 'url' => 'https://domains.'.$root, 'icon' => 'domains.svg'],
['name' => 'Servers', 'url' => 'https://servers.'.$root, 'icon' => 'servers.svg'], ['name' => 'Servers', 'url' => 'https://servers.'.$root, 'icon' => 'servers.svg'],
['name' => 'Hosting', 'url' => 'https://hosting.'.$root, 'icon' => 'hosting.svg'], ['name' => 'Hosting', 'url' => 'https://hosting.'.$root, 'icon' => 'hosting.svg'],
['name' => 'QR Plus', 'url' => 'https://qrplus.'.$root, 'icon' => 'qrplus.svg'], ['name' => 'QR Plus', 'url' => 'https://qrplus.'.$root.'/sso/connect?redirect='.urlencode('https://qrplus.'.$root.'/dashboard'), 'icon' => 'qrplus.svg'],
['name' => 'Events', 'url' => 'https://events.'.$root, 'icon' => 'events.svg'], ['name' => 'Events', 'url' => 'https://events.'.$root.'/sso/connect?redirect='.urlencode('https://events.'.$root.'/dashboard'), 'icon' => 'events.svg'],
], ],
]; ];
+1 -1
View File
@@ -23,7 +23,7 @@ return [
'issuer' => 'https://'.config('app.auth_domain'), 'issuer' => 'https://'.config('app.auth_domain'),
'client_id' => env('LADILL_SSO_CLIENT_ID'), 'client_id' => env('LADILL_SSO_CLIENT_ID'),
'client_secret' => env('LADILL_SSO_CLIENT_SECRET'), 'client_secret' => env('LADILL_SSO_CLIENT_SECRET'),
'redirect' => rtrim((string) env('APP_URL', 'https://qrplus.ladill.com'), '/').'/sso/callback', 'redirect' => rtrim((string) env('APP_URL', 'https://events.ladill.com'), '/').'/sso/callback',
], ],
'ladill_webmail' => [ 'ladill_webmail' => [
@@ -1,4 +1,13 @@
@if (auth()->check()) @if (auth()->check())
{{-- Same-site iframe keeps the shared auth.ladill.com session warm while using this app. --}} @php $authPing = 'https://'.config('app.auth_domain').'/sso/ping'; @endphp
<iframe src="https://{{ config('app.auth_domain') }}/sso/ping" hidden width="0" height="0" title=""></iframe> {{-- Same-site pings keep the shared auth.ladill.com session warm while using this app. --}}
<iframe src="{{ $authPing }}" hidden width="0" height="0" title=""></iframe>
<script>
(function () {
const pingUrl = @js($authPing);
const ping = () => fetch(pingUrl, { credentials: 'include', mode: 'no-cors' }).catch(() => {});
ping();
setInterval(ping, 5 * 60 * 1000);
})();
</script>
@endif @endif