Unify SSO with platform for seamless sign-in across Ladill apps.
Deploy Ladill QR Plus / deploy (push) Successful in 29s
Deploy Ladill QR Plus / deploy (push) Successful in 29s
Add silent OAuth, deep-link guest redirects, and a session keepalive iframe. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,19 +20,21 @@ class SsoLoginController extends Controller
|
|||||||
{
|
{
|
||||||
public function connect(Request $request): RedirectResponse
|
public function connect(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
|
$intended = (string) $request->query('redirect', route('qr.dashboard'));
|
||||||
|
|
||||||
if (Auth::check()) {
|
if (Auth::check()) {
|
||||||
return redirect()->route('qr.dashboard');
|
return $this->safeRedirect($intended, route('qr.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);
|
||||||
$request->session()->put('sso.state', $state);
|
$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)), '+/', '-_'), '=');
|
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
|
||||||
|
|
||||||
$query = http_build_query([
|
$query = [
|
||||||
'response_type' => 'code',
|
'response_type' => 'code',
|
||||||
'client_id' => (string) config('services.ladill_sso.client_id'),
|
'client_id' => (string) config('services.ladill_sso.client_id'),
|
||||||
'redirect_uri' => (string) config('services.ladill_sso.redirect'),
|
'redirect_uri' => (string) config('services.ladill_sso.redirect'),
|
||||||
@@ -40,18 +42,40 @@ class SsoLoginController extends Controller
|
|||||||
'state' => $state,
|
'state' => $state,
|
||||||
'code_challenge' => $challenge,
|
'code_challenge' => $challenge,
|
||||||
'code_challenge_method' => 'S256',
|
'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
|
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')) {
|
|| $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'), '/');
|
$issuer = rtrim((string) config('services.ladill_sso.issuer'), '/');
|
||||||
@@ -65,12 +89,18 @@ class SsoLoginController extends Controller
|
|||||||
'code_verifier' => (string) $request->session()->pull('sso.verifier'),
|
'code_verifier' => (string) $request->session()->pull('sso.verifier'),
|
||||||
]);
|
]);
|
||||||
if ($tokenRes->failed()) {
|
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');
|
$claims = Http::withToken((string) $tokenRes->json('access_token'))->acceptJson()->get($issuer.'/oauth/userinfo');
|
||||||
if ($claims->failed() || ! $claims->json('sub')) {
|
if ($claims->failed() || ! $claims->json('sub')) {
|
||||||
return redirect()->away($authLogin);
|
return redirect()->route('sso.connect', [
|
||||||
|
'redirect' => $intended,
|
||||||
|
'interactive' => 1,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = User::updateOrCreate(
|
$user = User::updateOrCreate(
|
||||||
@@ -87,7 +117,7 @@ class SsoLoginController extends Controller
|
|||||||
Auth::login($user, remember: true);
|
Auth::login($user, remember: true);
|
||||||
$request->session()->regenerate();
|
$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
|
public function logout(Request $request): RedirectResponse
|
||||||
@@ -110,8 +140,6 @@ class SsoLoginController extends Controller
|
|||||||
$request->session()->regenerateToken();
|
$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', '');
|
$return = (string) $request->query('return', '');
|
||||||
$root = (string) config('app.platform_domain', 'ladill.com');
|
$root = (string) config('app.platform_domain', 'ladill.com');
|
||||||
$host = parse_url($return, PHP_URL_HOST);
|
$host = parse_url($return, PHP_URL_HOST);
|
||||||
@@ -136,4 +164,17 @@ class SsoLoginController extends Controller
|
|||||||
|
|
||||||
return strtolower((string) $user->email) !== $mailbox;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
@@ -12,6 +13,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->redirectGuestsTo(fn (Request $request) => route('sso.connect', [
|
||||||
|
'redirect' => $request->fullUrl(),
|
||||||
|
]));
|
||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
\App\Http\Middleware\SetActingAccount::class,
|
\App\Http\Middleware\SetActingAccount::class,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -424,5 +424,6 @@ document.addEventListener('keydown', (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@include('partials.sso-keepalive')
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@if (auth()->check())
|
||||||
|
{{-- Same-site iframe keeps the shared auth.ladill.com session warm while using this app. --}}
|
||||||
|
<iframe src="https://{{ config('app.auth_domain') }}/sso/ping" hidden width="0" height="0" title=""></iframe>
|
||||||
|
@endif
|
||||||
Reference in New Issue
Block a user