presseportale/app/Services/Auth/SocialAuthService.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

93 lines
3.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\Services\Auth;
use App\Models\User;
use Illuminate\Support\Str;
/**
* Auflösung eines Social-Logins (Google, ggf. weitere) auf einen lokalen User.
*
* Identität ist die E-Mail (De-Dup darüber, keine Dubletten). Der Provider
* bestätigt die E-Mail, daher gilt die Verifizierung über den Kanal als erfüllt
* (Entscheidung 15.06.) – kein zusätzlicher E-Mail-Verifizierungsschritt.
*/
class SocialAuthService
{
public function __construct(
private readonly UserRolePermissionSyncService $roleSync,
private readonly CustomerAccountProvisioner $accountProvisioner,
private readonly SessionRevocation $sessions,
) {}
/**
* Liefert den (ggf. neu angelegten) User zur Social-Identität. Ein
* deaktivierter Bestands-Account wird NICHT reaktiviert – der Aufrufer prüft
* danach is_active und blockiert.
*/
public function resolveUser(string $provider, string $providerId, ?string $email, ?string $name): ?User
{
$email = $email ? mb_strtolower(trim($email)) : null;
if ($email === null || $email === '') {
return null;
}
$user = User::findByEmail($email);
if (! $user) {
return $this->createUser($provider, $providerId, $email, $name);
}
$user->forceFill([
'oauth_provider' => $provider,
'oauth_provider_id' => $providerId,
]);
// Noch nicht verifiziert (offener Selbst-Registrierer): der Provider
// bestätigt die E-Mail → Onboarding abschließen (aktiv + customer).
//
// Vorab-Übernahme (Audit K-04): Wer die Adresse unverifiziert
// registriert hat, war nicht zwingend der Inhaber des Postfachs. Alles,
// was er dabei gesetzt hat, verfällt: Passwort, Remember-Cookie,
// API-Tokens und offene Sessions. Der Inhaber meldet sich per Google
// oder Magic Link an und setzt bei Bedarf selbst ein Passwort.
if ($user->email_verified_at === null) {
$user->forceFill([
'email_verified_at' => now(),
'is_active' => true,
'password' => null,
]);
$user->setRememberToken(Str::random(60));
$user->save();
$user->tokens()->delete();
$this->sessions->revokeAllFor($user);
if ($user->roles()->doesntExist()) {
$this->roleSync->assignRoleAndSyncPermissions($user, 'customer');
}
return $user;
}
// Bereits verifiziert: nur Provider verknüpfen. is_active bleibt
// unangetastet (deaktivierte Accounts werden nicht reaktiviert).
$user->save();
return $user;
}
private function createUser(string $provider, string $providerId, string $email, ?string $name): User
{
return $this->accountProvisioner->provisionVerifiedCustomer(
$email,
$name !== null ? trim($name) : '',
[
'oauth_provider' => $provider,
'oauth_provider_id' => $providerId,
],
channel: $provider,
);
}
}