query('redirect', route('servers.dashboard')); if (Auth::check()) { return $this->safeRedirect($intended, route('servers.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', $intended); $challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '='); $query = [ 'response_type' => 'code', 'client_id' => (string) config('services.ladill_sso.client_id'), 'redirect_uri' => (string) config('services.ladill_sso.redirect'), 'scope' => 'openid profile email', 'state' => $state, 'code_challenge' => $challenge, 'code_challenge_method' => 'S256', ]; if (! $request->boolean('interactive')) { $query['prompt'] = 'none'; } $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|View|View { $intended = (string) $request->session()->get('sso.intended', route('servers.dashboard')); 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 $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 $this->finishCallback($request, $intended, 'invalid_state'); } $issuer = rtrim((string) config('services.ladill_sso.issuer'), '/'); $tokenRes = Http::asForm()->post($issuer.'/oauth/token', [ 'grant_type' => 'authorization_code', 'client_id' => (string) config('services.ladill_sso.client_id'), 'client_secret' => (string) config('services.ladill_sso.client_secret'), 'redirect_uri' => (string) config('services.ladill_sso.redirect'), 'code' => (string) $request->query('code'), 'code_verifier' => (string) $request->session()->pull('sso.verifier'), ]); if ($tokenRes->failed()) { return $this->finishCallback($request, $intended, 'token_exchange_failed'); } $claims = Http::withToken((string) $tokenRes->json('access_token'))->acceptJson()->get($issuer.'/oauth/userinfo'); if ($claims->failed() || ! $claims->json('sub')) { return $this->finishCallback($request, $intended, 'userinfo_failed'); } $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'), ], ); \App\Models\HostingTeamMember::linkPendingInvitesFor($user); Auth::login($user, remember: true); $request->session()->regenerate(); $request->session()->forget('mailbox_link_banner_dismissed'); return $this->finishCallback($request, $intended); } public function logout(Request $request): RedirectResponse { Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('sso.logout-bridge', [ 'return' => $this->defaultSignedOutUrl(), ]); } 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 { if ($this->shouldLogoutForMailbox($request)) { Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); } $return = (string) $request->query('return', ''); $root = (string) config('app.platform_domain', 'ladill.com'); $host = parse_url($return, PHP_URL_HOST); if (str_starts_with($return, 'https://') && is_string($host) && ($host === $root || str_ends_with($host, '.'.$root))) { return redirect()->away($return); } return response('', 204); } private function shouldLogoutForMailbox(Request $request): bool { $mailbox = strtolower(trim((string) $request->query('mailbox', ''))); if ($mailbox === '' || ! str_contains($mailbox, '@')) { return true; } $user = Auth::user(); if (! $user) { return false; } 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->finishCallback($request, $intended); } 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); $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); } }