Improve cross-app SSO when switching from QR Plus to Events.
Deploy Ladill Events / deploy (push) Successful in 26s
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:
@@ -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', '')));
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user