Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions app/Http/Controllers/Auth/GoogleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
]);
}

Expand All @@ -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') {
Expand All @@ -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');
Expand Down
62 changes: 39 additions & 23 deletions app/Http/Requests/Auth/LoginRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
Expand All @@ -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([
Expand All @@ -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)) {
Expand All @@ -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());
}
}
}
Loading