diff --git a/app/Http/Controllers/Auth/SsoLoginController.php b/app/Http/Controllers/Auth/SsoLoginController.php index 0cf2074..1b658ed 100644 --- a/app/Http/Controllers/Auth/SsoLoginController.php +++ b/app/Http/Controllers/Auth/SsoLoginController.php @@ -20,19 +20,21 @@ class SsoLoginController extends Controller { public function connect(Request $request): RedirectResponse { + $intended = (string) $request->query('redirect', route('qr.dashboard')); + if (Auth::check()) { - return redirect()->route('qr.dashboard'); + return $this->safeRedirect($intended, route('qr.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('qr.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'), @@ -40,18 +42,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('qr.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'), '/'); @@ -65,12 +89,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( @@ -87,7 +117,7 @@ class SsoLoginController extends Controller Auth::login($user, remember: true); $request->session()->regenerate(); - return redirect()->intended($request->session()->pull('sso.intended', route('qr.dashboard'))); + return $this->safeRedirect($intended, route('qr.dashboard')); } public function logout(Request $request): RedirectResponse @@ -110,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); @@ -136,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); + } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 9c29590..8e63454 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Http\Request; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -12,6 +13,9 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { + $middleware->redirectGuestsTo(fn (Request $request) => route('sso.connect', [ + 'redirect' => $request->fullUrl(), + ])); $middleware->web(append: [ \App\Http\Middleware\SetActingAccount::class, ]); diff --git a/resources/views/layouts/user.blade.php b/resources/views/layouts/user.blade.php index 17e0fb5..8f0dc46 100644 --- a/resources/views/layouts/user.blade.php +++ b/resources/views/layouts/user.blade.php @@ -424,5 +424,6 @@ document.addEventListener('keydown', (e) => { } }); +@include('partials.sso-keepalive') diff --git a/resources/views/partials/sso-keepalive.blade.php b/resources/views/partials/sso-keepalive.blade.php new file mode 100644 index 0000000..ecd5d4a --- /dev/null +++ b/resources/views/partials/sso-keepalive.blade.php @@ -0,0 +1,4 @@ +@if (auth()->check()) + {{-- Same-site iframe keeps the shared auth.ladill.com session warm while using this app. --}} + +@endif