presseportale/app/Services/Auth/LoginCompletion.php
Kevin Adametz 1ccb4a8479 auth/security: API-Firmenzuordnung nur für Mitglieder oder Allowlist, Google-Vorabübernahme geschlossen, gemeinsamer Login-Abschluss (Umsetzungsaudit 29.09.: R-01, K-04, K-07, K-08, B-10)
- R-01: ApiNewsroomAssignment hängt an eine bestehende Firma nur noch Owner,
  Mitglieder oder Nutzer aus NEWSROOM_API_ASSIGNMENT_USER_IDS; Pivot mit
  granted_via = api; Befehl company-memberships:api-assignments (Liste,
  --remove mit Vorschau)
- K-04: OAuth-Verifikation eines offenen Kontos verwirft Passwort,
  remember_token und Sanctum-Tokens; SessionRevocation + Middleware
  EnsureSessionIsNotRevoked widerruft alte Sessions (Redis-tauglich)
- K-08: LoginCompletion für Passwort, Magic Link und Google — last_login_at
  und Anmeldemethode auch nach 2FA, Landeziel über die Challenge, Google
  ohne erzwungenes remember, magic-login ohne guest für eigene Links
- B-10: eigener Magic-Link-Zweck image_confirmation (Migration)
- K-07: legalAcceptanceVersions #[Locked], Versionswechsel beim Absenden
  wird abgelehnt, Gate-Abfragen je Request zwischengespeichert

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

98 lines
3.4 KiB
PHP

<?php
namespace App\Services\Auth;
use App\Listeners\RecordAuthActivity;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
/**
* Gemeinsamer Abschluss der drei Anmeldewege Passwort, Magic Link und
* Google (Plan 1.8, Umsetzungsaudit 29.09.2026 K-08).
*
* - Direkter Login: Anmeldeweg fürs Aktivitäts-Log, Session-Login,
* Session-ID erneuern, `last_login_at`/`last_login_ip`.
* - Übergabe an die 2FA-Challenge: derselbe Session-Vertrag wie Fortify
* (`login.id`, `login.remember`), zusätzlich der Anmeldeweg und bei Bedarf
* das Landeziel (`url.intended`). Nach der Challenge liest
* RecordAuthActivity den Anmeldeweg, RoleAwareLoginResponse setzt
* `last_login_at` und folgt dem Landeziel.
*
* „Angemeldet bleiben" gibt es nur beim Passwort-Login als bewusste Wahl im
* Formular. Magic Link und Google setzen keinen Remember-Cookie: Beide
* beweisen den Besitz des Postfachs bzw. des Google-Kontos für diese eine
* Anmeldung, ein Jahres-Cookie folgt daraus nicht.
*/
class LoginCompletion
{
/**
* Anmeldeweg während einer offenen 2FA-Challenge.
*/
public const METHOD_SESSION_KEY = 'login.method';
public function login(Request $request, User $user, string $method, bool $remember = false): void
{
$request->attributes->set(RecordAuthActivity::AUTH_METHOD_ATTRIBUTE, $method);
Session::forget(self::METHOD_SESSION_KEY);
Auth::guard('web')->login($user, $remember);
Session::regenerate();
$this->recordLogin($user, $request);
}
/**
* Übergabe an Fortifys 2FA-Challenge statt eines direkten Logins.
*
* @param string|null $intended Landeziel nach bestandener Challenge
* (wird von RoleAwareLoginResponse nur
* übernommen, wenn der Nutzer es
* erreichen darf).
*/
public function handOffToTwoFactor(Request $request, User $user, string $method, bool $remember = false, ?string $intended = null): void
{
// Session-Fassade statt $request->session(): auch aus Livewire-Aktionen
// (Volt-Login) nutzbar, die denselben Store verwenden.
Session::put([
'login.id' => $user->getKey(),
'login.remember' => $remember,
self::METHOD_SESSION_KEY => $method,
]);
if ($intended !== null) {
Session::put('url.intended', $intended);
}
Session::regenerate();
}
public function recordLogin(User $user, Request $request): void
{
$user->forceFill([
'last_login_at' => now(),
'last_login_ip' => $request->ip(),
])->save();
}
/**
* Anmeldeweg des gerade laufenden Logins: vom einloggenden Code gesetzt,
* beim Abschluss der 2FA-Challenge aus der Session, sonst Passwort
* (Fortify-/Volt-Login).
*/
public static function methodOf(Request $request): string
{
$method = $request->attributes->get(RecordAuthActivity::AUTH_METHOD_ATTRIBUTE);
if (is_string($method) && $method !== '') {
return $method;
}
if ($request->routeIs('two-factor.login.store')) {
return (string) Session::pull(self::METHOD_SESSION_KEY, 'password');
}
return 'password';
}
}