feat: add rate limiting, case-insensitive usernames and session security

This commit is contained in:
2026-02-21 14:03:34 +01:00
parent 638a27208f
commit 22a6bc5123
5 changed files with 26 additions and 8 deletions

View File

@@ -43,7 +43,15 @@ class CompleteProfileController extends Controller
} }
$request->validate([ $request->validate([
'username' => ['required', 'string', 'max:255', 'alpha_dash', 'unique:'.User::class], 'username' => [
'required', 'string', 'max:255', 'alpha_dash',
function ($attribute, $value, $fail) {
$exists = User::whereRaw('LOWER(username) = ?', [strtolower($value)])->exists();
if ($exists) {
$fail('The username has already been taken.');
}
},
],
'first_name' => ['required', 'string', 'max:255'], 'first_name' => ['required', 'string', 'max:255'],
'last_name' => ['required', 'string', 'max:255'], 'last_name' => ['required', 'string', 'max:255'],
]); ]);
@@ -62,6 +70,7 @@ class CompleteProfileController extends Controller
event(new Registered($user)); event(new Registered($user));
Auth::login($user, remember: true); Auth::login($user, remember: true);
$request->session()->regenerate();
return redirect()->intended(config('auth-ui.redirects.login', '/')); return redirect()->intended(config('auth-ui.redirects.login', '/'));
} }

View File

@@ -37,7 +37,15 @@ class RegisterController extends Controller
} }
$request->validate([ $request->validate([
'username' => ['required', 'string', 'max:255', 'alpha_dash', 'unique:'.User::class], 'username' => [
'required', 'string', 'max:255', 'alpha_dash',
function ($attribute, $value, $fail) {
$exists = User::whereRaw('LOWER(username) = ?', [strtolower($value)])->exists();
if ($exists) {
$fail('The username has already been taken.');
}
},
],
'first_name' => ['required', 'string', 'max:255'], 'first_name' => ['required', 'string', 'max:255'],
'last_name' => ['required', 'string', 'max:255'], 'last_name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],

View File

@@ -79,7 +79,7 @@ class SocialiteController extends Controller
$suggestedUsername = $this->suggestUsername($socialUser); $suggestedUsername = $this->suggestUsername($socialUser);
// Check if username is already taken // Check if username is already taken
if (User::where('username', $suggestedUsername)->exists()) { if (User::whereRaw('LOWER(username) = ?', [strtolower($suggestedUsername)])->exists()) {
// Store social data in session and redirect to complete profile // Store social data in session and redirect to complete profile
session()->put('socialite_user', [ session()->put('socialite_user', [
'email' => $socialUser->getEmail(), 'email' => $socialUser->getEmail(),
@@ -103,6 +103,7 @@ class SocialiteController extends Controller
} }
Auth::login($user, remember: true); Auth::login($user, remember: true);
request()->session()->regenerate();
return redirect()->intended(config('auth-ui.redirects.login', '/')); return redirect()->intended(config('auth-ui.redirects.login', '/'));
} }

View File

@@ -46,7 +46,7 @@ class HandleInertiaRequests extends Middleware
'message' => fn () => $request->session()->get('message'), 'message' => fn () => $request->session()->get('message'),
'status' => fn () => $request->session()->get('status'), 'status' => fn () => $request->session()->get('status'),
], ],
'authConfig' => $this->getAuthConfig(), 'authConfig' => fn () => $this->getAuthConfig(),
]; ];
} }

View File

@@ -10,16 +10,16 @@ use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () { Route::middleware('guest')->group(function () {
Route::get('login', [LoginController::class, 'create'])->name('login'); Route::get('login', [LoginController::class, 'create'])->name('login');
Route::post('login', [LoginController::class, 'store']); Route::post('login', [LoginController::class, 'store'])->middleware('throttle:5,1');
Route::get('register', [RegisterController::class, 'create'])->name('register'); Route::get('register', [RegisterController::class, 'create'])->name('register');
Route::post('register', [RegisterController::class, 'store']); Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:3,60');
Route::get('forgot-password', [ForgotPasswordController::class, 'create'])->name('password.request'); Route::get('forgot-password', [ForgotPasswordController::class, 'create'])->name('password.request');
Route::post('forgot-password', [ForgotPasswordController::class, 'store'])->name('password.email'); Route::post('forgot-password', [ForgotPasswordController::class, 'store'])->name('password.email')->middleware('throttle:3,15');
Route::get('reset-password/{token}', [ResetPasswordController::class, 'create'])->name('password.reset'); Route::get('reset-password/{token}', [ResetPasswordController::class, 'create'])->name('password.reset');
Route::post('reset-password', [ResetPasswordController::class, 'store'])->name('password.store'); Route::post('reset-password', [ResetPasswordController::class, 'store'])->name('password.store')->middleware('throttle:5,15');
// Socialite routes // Socialite routes
Route::get('auth/{provider}', [SocialiteController::class, 'redirect'])->name('socialite.redirect'); Route::get('auth/{provider}', [SocialiteController::class, 'redirect'])->name('socialite.redirect');