diff --git a/app/Http/Controllers/Auth/GoogleController.php b/app/Http/Controllers/Auth/GoogleController.php index 3bb85d0..f12e409 100644 --- a/app/Http/Controllers/Auth/GoogleController.php +++ b/app/Http/Controllers/Auth/GoogleController.php @@ -27,10 +27,13 @@ public function register() public function callback(Request $request) { try { - $googleUser = Socialite::driver('google')->user(); + // Using stateless() prevents the InvalidStateException caused by + // dropped cookies when routing through tunnels like Expose or Ngrok. + $googleUser = Socialite::driver('google')->stateless()->user(); } catch (\Exception $e) { + \Log::error('Google Auth Failed: ' . $e->getMessage()); return redirect()->route('login')->withErrors([ - 'email' => 'Google authentication was cancelled or failed. Please try again.', + 'email' => 'Google authentication failed. Please try again.', ]); } @@ -40,24 +43,22 @@ public function callback(Request $request) ->orWhere('email', $googleUser->getEmail()) ->first(); - if ($intent === 'register') { - if ($existingUser) { - return redirect()->route('login')->withErrors([ - 'email' => 'An account with this email already exists. Please log in instead.', + if ($intent === 'login') { + if (!$existingUser) { + return redirect()->route('register')->withErrors([ + 'email' => 'No account found with this Google account. Please register first.', ]); } - $newUser = User::create([ - 'name' => $googleUser->getName(), - 'email' => $googleUser->getEmail(), + $existingUser->update([ 'google_id' => $googleUser->getId(), 'avatar' => $googleUser->getAvatar(), - 'password' => Hash::make(Str::random(32)), ]); - Auth::login($newUser, true); + Auth::login($existingUser, true); + $request->session()->regenerate(); // CRITICAL: Persists the session state - return redirect()->route('dashboard'); + return redirect()->intended(route('dashboard')); } if ($intent === 'register') { @@ -73,12 +74,12 @@ public function callback(Request $request) 'google_id' => $googleUser->getId(), 'avatar' => $googleUser->getAvatar(), 'password' => Hash::make(Str::random(32)), - 'api_token' => Str::random(60), ]); Auth::login($newUser, true); + $request->session()->regenerate(); // CRITICAL: Persists the session state - return redirect()->route('dashboard'); + return redirect()->intended(route('dashboard')); } return redirect()->route('login'); diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index 711e0a1..953b893 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -2,29 +2,24 @@ namespace App\Http\Requests\Auth; +use App\Models\User; use Illuminate\Auth\Events\Lockout; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; +use RuntimeException; class LoginRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ public function rules(): array { return [ @@ -33,16 +28,45 @@ public function rules(): array ]; } - /** - * Attempt to authenticate the request's credentials. - * - * @throws ValidationException - */ public function authenticate(): void { $this->ensureIsNotRateLimited(); - if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + $user = User::where('email', $this->input('email'))->first(); + + // Intercept OAuth-provisioned accounts to prevent password fallback bypass + if ($user && !empty($user->google_id)) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('This account is secured with Google Sign-In. Please authenticate using the Google provider.'), + ]); + } + + // Seamlessly upgrade legacy Bcrypt hashes to Argon2id + if ($user && !empty($user->password) && str_starts_with($user->password, '$2y$')) { + if (password_verify($this->input('password'), $user->password)) { + $user->update([ + 'password' => Hash::make($this->input('password')) + ]); + } else { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.failed'), + ]); + } + } + + try { + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.failed'), + ]); + } + } catch (RuntimeException $e) { RateLimiter::hit($this->throttleKey()); throw ValidationException::withMessages([ @@ -53,11 +77,6 @@ public function authenticate(): void RateLimiter::clear($this->throttleKey()); } - /** - * Ensure the login request is not rate limited. - * - * @throws ValidationException - */ public function ensureIsNotRateLimited(): void { if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { @@ -76,11 +95,8 @@ public function ensureIsNotRateLimited(): void ]); } - /** - * Get the rate limiting throttle key for the request. - */ public function throttleKey(): string { return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); } -} +} \ No newline at end of file