getKey() && (int) ($confirmed['at'] ?? 0) >= now()->getTimestamp() - self::FRESH_FOR_SECONDS; } public function markConfirmed(User $user): void { Session::put(self::SESSION_KEY, [ 'user_id' => $user->getKey(), 'at' => now()->getTimestamp(), ]); } /** * Schickt einen 6-stelligen Code an die aktuelle Kontoadresse. Liefert * false, wenn zu viele Codes angefordert wurden (3 je 10 Minuten). */ public function sendEmailCode(User $user): bool { $throttleKey = 'security-email-code-send:'.$user->getKey(); if (RateLimiter::tooManyAttempts($throttleKey, 3)) { return false; } RateLimiter::hit($throttleKey, 600); $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); $expiresAt = now()->addMinutes(self::CODE_VALID_MINUTES); Cache::put($this->codeKey($user), [ 'hash' => $this->hash($code), 'attempts' => 0, 'expires_at' => $expiresAt->getTimestamp(), ], $expiresAt); Mail::to($user->email)->queue((new SecurityConfirmationCode( user: $user, code: $code, expiresAt: $expiresAt->format('H:i'), ))->afterCommit()); return true; } /** * Prüft den Code; er ist nur einmal gültig und nach * MAX_CODE_ATTEMPTS Fehlversuchen verbraucht. */ public function verifyEmailCode(User $user, string $code): bool { $key = $this->codeKey($user); $entry = Cache::get($key); if (! is_array($entry) || (int) ($entry['expires_at'] ?? 0) < now()->getTimestamp()) { return false; } if (hash_equals((string) $entry['hash'], $this->hash(trim($code)))) { Cache::forget($key); return true; } $entry['attempts'] = (int) $entry['attempts'] + 1; if ($entry['attempts'] >= self::MAX_CODE_ATTEMPTS) { Cache::forget($key); } else { Cache::put($key, $entry, now()->setTimestamp((int) $entry['expires_at'])); } return false; } private function codeKey(User $user): string { return 'security-email-code:'.$user->getKey(); } private function hash(string $code): string { return hash_hmac('sha256', $code, (string) config('app.key')); } }