Keep the app subdomain visible during Ladill SSO login and logout.
Deploy Ladill Events / deploy (push) Successful in 39s
Deploy Ladill Events / deploy (push) Successful in 39s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,7 +11,9 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* "Sign in with Ladill" — Authorization Code + PKCE against auth.ladill.com.
|
||||
@@ -19,7 +21,7 @@ use Illuminate\Support\Str;
|
||||
*/
|
||||
class SsoLoginController extends Controller
|
||||
{
|
||||
public function connect(Request $request): RedirectResponse
|
||||
public function connect(Request $request): RedirectResponse|View
|
||||
{
|
||||
$intended = (string) $request->query('redirect', route('events.dashboard'));
|
||||
|
||||
@@ -58,10 +60,26 @@ class SsoLoginController extends Controller
|
||||
$query['prompt'] = 'none';
|
||||
}
|
||||
|
||||
return redirect()->away(rtrim((string) config('services.ladill_sso.issuer'), '/').'/oauth/authorize?'.http_build_query($query));
|
||||
$authorizeUrl = rtrim((string) config('services.ladill_sso.issuer'), '/').'/oauth/authorize?'.http_build_query($query);
|
||||
|
||||
if ($request->boolean('interactive') && ! $request->boolean('fallback')) {
|
||||
$request->session()->put('sso.popup', true);
|
||||
|
||||
return view('auth.sso-signing-in', [
|
||||
'authorizeUrl' => $authorizeUrl,
|
||||
'intended' => $intended,
|
||||
'fallbackUrl' => route('sso.connect', [
|
||||
'redirect' => $intended,
|
||||
'interactive' => 1,
|
||||
'fallback' => 1,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->away($authorizeUrl);
|
||||
}
|
||||
|
||||
public function callback(Request $request): RedirectResponse
|
||||
public function callback(Request $request): RedirectResponse|View
|
||||
{
|
||||
$intended = (string) $request->session()->get('sso.intended', route('events.dashboard'));
|
||||
|
||||
@@ -74,18 +92,12 @@ class SsoLoginController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('sso.connect', [
|
||||
'redirect' => $intended,
|
||||
'interactive' => 1,
|
||||
]);
|
||||
return $this->finishCallback($request, $intended, (string) $request->query('error_description', $request->query('error')));
|
||||
}
|
||||
|
||||
if (! $request->filled('code')
|
||||
|| $request->query('state') !== $request->session()->pull('sso.state')) {
|
||||
return redirect()->route('sso.connect', [
|
||||
'redirect' => $intended,
|
||||
'interactive' => 1,
|
||||
]);
|
||||
return $this->finishCallback($request, $intended, 'invalid_state');
|
||||
}
|
||||
|
||||
$issuer = rtrim((string) config('services.ladill_sso.issuer'), '/');
|
||||
@@ -99,18 +111,12 @@ class SsoLoginController extends Controller
|
||||
'code_verifier' => (string) $request->session()->pull('sso.verifier'),
|
||||
]);
|
||||
if ($tokenRes->failed()) {
|
||||
return redirect()->route('sso.connect', [
|
||||
'redirect' => $intended,
|
||||
'interactive' => 1,
|
||||
]);
|
||||
return $this->finishCallback($request, $intended, 'token_exchange_failed');
|
||||
}
|
||||
|
||||
$user = $this->loginFromTokenResponse($request, $tokenRes);
|
||||
if (! $user) {
|
||||
return redirect()->route('sso.connect', [
|
||||
'redirect' => $intended,
|
||||
'interactive' => 1,
|
||||
]);
|
||||
return $this->finishCallback($request, $intended, 'userinfo_failed');
|
||||
}
|
||||
|
||||
QrTeamMember::linkPendingInvitesFor($user);
|
||||
@@ -118,7 +124,7 @@ class SsoLoginController extends Controller
|
||||
Auth::login($user, remember: true);
|
||||
$request->session()->regenerate();
|
||||
|
||||
return $this->safeRedirect($intended, route('events.dashboard'));
|
||||
return $this->finishCallback($request, $intended);
|
||||
}
|
||||
|
||||
public function logout(Request $request): RedirectResponse
|
||||
@@ -127,10 +133,24 @@ class SsoLoginController extends Controller
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
$home = 'https://'.config('app.platform_domain');
|
||||
$endSession = 'https://'.config('app.auth_domain').'/logout/sso?redirect='.urlencode($home);
|
||||
return redirect()->route('sso.logout-bridge', [
|
||||
'return' => $this->defaultSignedOutUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->away($endSession);
|
||||
public function logoutBridge(Request $request): View
|
||||
{
|
||||
$return = $this->safeReturnUrl((string) $request->query('return', ''));
|
||||
$hubUrl = 'https://'.config('app.auth_domain').'/logout/sso/hub?'.http_build_query([
|
||||
'embedded' => 1,
|
||||
'return' => $return,
|
||||
]);
|
||||
|
||||
return view('auth.sso-logout-bridge', [
|
||||
'hubUrl' => $hubUrl,
|
||||
'return' => $return,
|
||||
'authOrigin' => 'https://'.config('app.auth_domain'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function frontchannelLogout(Request $request): Response|RedirectResponse
|
||||
@@ -228,6 +248,50 @@ class SsoLoginController extends Controller
|
||||
return strtolower((string) $user->email) !== $mailbox;
|
||||
}
|
||||
|
||||
private function finishCallback(Request $request, string $intended, ?string $error = null): RedirectResponse|View
|
||||
{
|
||||
if ($request->session()->pull('sso.popup')) {
|
||||
return view('auth.sso-popup-done', [
|
||||
'intended' => $intended,
|
||||
'error' => $error,
|
||||
'appOrigin' => rtrim((string) config('app.url'), '/'),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($error) {
|
||||
return redirect()->route('sso.connect', [
|
||||
'redirect' => $intended,
|
||||
'interactive' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->safeRedirect($intended, route('events.dashboard'));
|
||||
}
|
||||
|
||||
private function defaultSignedOutUrl(): string
|
||||
{
|
||||
foreach (array_keys(Route::getRoutes()->getRoutesByName()) as $name) {
|
||||
if (str_ends_with($name, '.signed-out')) {
|
||||
return route($name);
|
||||
}
|
||||
}
|
||||
|
||||
return 'https://'.config('app.platform_domain');
|
||||
}
|
||||
|
||||
private function safeReturnUrl(string $url): string
|
||||
{
|
||||
$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 $url;
|
||||
}
|
||||
|
||||
return $this->defaultSignedOutUrl();
|
||||
}
|
||||
|
||||
private function safeRedirect(string $url, string $fallback): RedirectResponse
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Signing out…</title>
|
||||
@include('partials.favicon')
|
||||
<meta name="robots" content="noindex">
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; display: flex; min-height: 100vh; align-items: center; justify-content: center; margin: 0; background: #f8fafc; color: #334155; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Signing you out of Ladill…</p>
|
||||
<iframe src="{{ $hubUrl }}" hidden width="0" height="0" title=""></iframe>
|
||||
<script>
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.origin !== @json($authOrigin)) return;
|
||||
if (event.data && event.data.type === 'ladill:logout:done' && event.data.return) {
|
||||
window.location.replace(event.data.return);
|
||||
}
|
||||
});
|
||||
setTimeout(function () { window.location.replace(@json($return)); }, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Signing in…</title>
|
||||
<meta name="robots" content="noindex">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
(function () {
|
||||
var intended = @json($intended);
|
||||
var appOrigin = @json($appOrigin);
|
||||
@if ($error)
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: 'ladill:sso:error' }, appOrigin);
|
||||
window.close();
|
||||
} else {
|
||||
window.location.replace(@json(route('sso.connect', ['redirect' => $intended, 'interactive' => 1, 'fallback' => 1])));
|
||||
}
|
||||
@else
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: 'ladill:sso:success', return: intended }, appOrigin);
|
||||
window.close();
|
||||
} else {
|
||||
window.location.replace(intended);
|
||||
}
|
||||
@endif
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Signing in…</title>
|
||||
@include('partials.favicon')
|
||||
<meta name="robots" content="noindex">
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; display: flex; min-height: 100vh; align-items: center; justify-content: center; margin: 0; background: #f8fafc; color: #334155; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Signing you in with Ladill…</p>
|
||||
<script>
|
||||
(function () {
|
||||
var intended = @json($intended);
|
||||
var authorizeUrl = @json($authorizeUrl);
|
||||
var fallbackUrl = @json($fallbackUrl);
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (!event.data) return;
|
||||
if (event.data.type === 'ladill:sso:success') {
|
||||
window.location.replace(event.data.return || intended);
|
||||
} else if (event.data.type === 'ladill:sso:error') {
|
||||
window.location.replace(fallbackUrl);
|
||||
}
|
||||
});
|
||||
|
||||
var popup = window.open(authorizeUrl, 'ladill_sso', 'width=520,height=720');
|
||||
if (!popup) {
|
||||
window.location.replace(fallbackUrl);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -24,6 +24,7 @@ Route::get('/login', [SsoLoginController::class, 'connect'])->name('login');
|
||||
Route::get('/sso/connect', [SsoLoginController::class, 'connect'])->name('sso.connect');
|
||||
Route::get('/sso/callback', [SsoLoginController::class, 'callback'])->name('sso.callback');
|
||||
Route::post('/logout', [SsoLoginController::class, 'logout'])->name('logout');
|
||||
Route::get('/sso/logout-bridge', [SsoLoginController::class, 'logoutBridge'])->name('sso.logout-bridge');
|
||||
Route::get('/sso/logout-frontchannel', [SsoLoginController::class, 'frontchannelLogout'])->name('sso.logout-frontchannel');
|
||||
Route::get('/signed-out', fn () => auth()->check() ? redirect()->route('events.dashboard') : view('events.signed-out'))->name('events.signed-out');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user