presseportale/app/Http/Controllers/Auth/GoogleController.php
Kevin Adametz bae8347f6c security: Sicherheitsaudit 07.10.2026 (S-01–S-06) behoben, Passkeys und Zweitfaktor-Pflicht für Admins
Audit: dev/sicherheit/SICHERHEITSAUDIT-2026-10-07.md, Plan und Entscheidungen:
dev/sicherheit/ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md.

- S-01 Legacy-Rechnungen fremder Kunden nur mit billing:manage (auch Kundenroute).
- S-02 Kontaktzuordnung auf der Firmenanzeige verlangt companies:write, $id gesperrt.
- S-03 CompanyMembershipObserver: automatische Firmenrechte werden bei jeder
  Adress-/Zuordnungsänderung sofort neu bewertet; memberships:resync-auto.
- S-04 Passwortwechsel beendet alle anderen Sessions (PasswordChanger), Reset
  alle Sessions und offene Login-Links; Fortify PUT /user/password abgeschaltet.
  API-Tokens bleiben bestehen.
- S-05 Google: email_verified Pflicht, feste Bindung über die Provider-ID;
  Erstverknüpfung bestehender Konten außerhalb gmail/hd nur per Bestätigungslink
  in derselben Browser-Session.
- S-06 Frischer Identitätsnachweis (Passwort, App-Code, E-Mail-Code oder
  Passkey) vor E-Mail-Wechsel, erster 2FA-Einrichtung und Token-Erzeugung;
  Hinweis-Mails an die (bisherige) Kontoadresse.
- Entscheidung 5: Passkeys (Fortify, @laravel/passkeys) für alle; Super-Admins
  und Admins bestätigen die Session für den Admin-Bereich per Passkey oder
  App-Code (EnsureStrongAdminAuthentication, Notfallschalter
  AUTH_REQUIRE_STRONG_ADMIN); Reset des zweiten Faktors durch Super-Admins.

Neue Migration: create_passkeys_table. Suite 2.403 grün (1 übersprungen),
Larastan ohne Meldung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:09:35 +00:00

123 lines
5.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\Auth\LoginCompletion;
use App\Services\Auth\SocialAccountLinker;
use App\Services\Auth\SocialAuthService;
use App\Services\Auth\SocialLoginResult;
use App\Support\LoginRedirect;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Laravel\Socialite\AbstractUser;
use Laravel\Socialite\Facades\Socialite;
/**
* Google-Login (WS-6). Erst-Login legt – de-dupliziert über die E-Mail – einen
* aktiven, verifizierten customer-Account an; bestehende Accounts werden
* verknüpft, bei Adressen außerhalb von Googles Zuständigkeit erst nach
* Bestätigung per E-Mail (Sicherheitsaudit 07.10.2026, S-05). Deaktivierte
* Accounts werden nicht reaktiviert.
*/
class GoogleController extends Controller
{
public function redirect(): RedirectResponse
{
return Socialite::driver('google')->redirect();
}
public function callback(Request $request, SocialAuthService $social, SocialAccountLinker $linker, LoginCompletion $completion): RedirectResponse
{
try {
$googleUser = Socialite::driver('google')->user();
} catch (\Throwable) {
return $this->failed(__('Die Anmeldung mit Google ist fehlgeschlagen. Bitte versuchen Sie es erneut.'));
}
$raw = $googleUser instanceof AbstractUser ? $googleUser->getRaw() : [];
$providerId = (string) $googleUser->getId();
$result = $social->resolve(
'google',
$providerId,
$googleUser->getEmail(),
$googleUser->getName(),
emailVerified: filter_var($raw['email_verified'] ?? false, FILTER_VALIDATE_BOOLEAN),
hostedDomain: isset($raw['hd']) ? (string) $raw['hd'] : null,
);
if ($result->outcome === SocialLoginResult::UNVERIFIED_EMAIL) {
return $this->failed(__('Google hat keine bestätigte E-Mail-Adresse übermittelt. Bitte melden Sie sich per E-Mail-Link an.'));
}
if ($result->outcome === SocialLoginResult::PROVIDER_MISMATCH) {
Log::warning('google-login.provider-mismatch', ['user_id' => $result->user?->getKey()]);
return $this->failed(__('Ihr Konto ist bereits mit einem anderen Google-Konto verknüpft. Bitte melden Sie sich per E-Mail-Link an.'));
}
$user = $result->user;
if (! $user || ! $user->is_active) {
return $this->failed(__('Ihr Konto ist nicht aktiv. Bitte wenden Sie sich an den Support.'));
}
if ($result->outcome === SocialLoginResult::CONFIRM_LINK) {
if (! $linker->requestConfirmation($user, 'google', $providerId)) {
return $this->failed(__('Zu viele Anfragen. Bitte warten Sie einige Minuten und versuchen Sie es erneut.'));
}
return redirect()->route('login')->with('status', __('Für Ihr bestehendes Konto ist eine einmalige Bestätigung nötig: Wir haben Ihnen einen Link an Ihre E-Mail-Adresse geschickt. Bitte öffnen Sie ihn in diesem Browser.'));
}
return $this->completeLogin($request, $user, $completion);
}
/**
* Bestätigungslink aus der Mail (SocialAccountLinker): verknüpft und
* meldet an — nur in der Browser-Session, die den Google-Login begonnen hat.
*/
public function confirmLink(Request $request, User $user, SocialAccountLinker $linker, LoginCompletion $completion): RedirectResponse
{
if (! $linker->confirm($user, (string) $request->query('provider_id', ''))) {
return $this->failed(__('Der Bestätigungslink ist ungültig oder abgelaufen. Bitte öffnen Sie ihn im selben Browser, in dem Sie die Anmeldung mit Google gestartet haben, oder starten Sie die Anmeldung erneut.'));
}
if (! $user->is_active) {
return $this->failed(__('Ihr Konto ist nicht aktiv. Bitte wenden Sie sich an den Support.'));
}
return $this->completeLogin($request, $user, $completion);
}
private function completeLogin(Request $request, User $user, LoginCompletion $completion): RedirectResponse
{
// 2FA aktiv: wie Passwort- und Magic-Link-Login nicht direkt einloggen,
// sondern in Fortifys Challenge übergeben (Audit F02). Google belegt
// den Besitz des Postfachs, nicht den zweiten Faktor.
//
// Kein erzwungenes „Angemeldet bleiben" mehr (Audit K-08): wie beim
// Magic Link nur die Sitzung, siehe LoginCompletion.
if ($user->hasEnabledTwoFactorAuthentication()) {
$completion->handOffToTwoFactor($request, $user, 'google');
return redirect()->route('two-factor.challenge');
}
$completion->login($request, $user, 'google');
return redirect(LoginRedirect::safeTarget(
$user,
$request->session()->pull('url.intended'),
LoginRedirect::homeFor($user),
));
}
private function failed(string $message): RedirectResponse
{
return redirect()->route('login')->withErrors(['email' => $message]);
}
}