diff --git a/.env.example b/.env.example index ad97994320..bd65c1935f 100644 --- a/.env.example +++ b/.env.example @@ -146,7 +146,13 @@ AWS_DEFAULT_REGION=null # -------------------------------------------- LOGIN_MAX_ATTEMPTS=5 LOGIN_LOCKOUT_DURATION=60 -RESET_PASSWORD_LINK_EXPIRES=900 + +# -------------------------------------------- +# OPTIONAL: FORGOTTEN PASSWORD SETTINGS +# -------------------------------------------- +RESET_PASSWORD_LINK_EXPIRES=15 +PASSWORD_CONFIRM_TIMEOUT=10800 +PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50 # -------------------------------------------- # OPTIONAL: MISC diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 9a5294c2d2..0b80d2eccd 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -24,6 +24,7 @@ class Kernel extends ConsoleKernel $schedule->command('snipeit:backup')->weekly(); $schedule->command('backup:clean')->daily(); $schedule->command('snipeit:upcoming-audits')->daily(); + $schedule->command('auth:clear-resets')->everyFifteenMinutes(); } /** diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 9e94740506..c16f00facb 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -136,13 +136,12 @@ class LoginController extends Controller // Better logging if (!$saml->isEnabled()) { - \Log::warning("SAML page requested, but SAML does not seem to enabled."); + \Log::debug("SAML page requested, but SAML does not seem to enabled."); } else { \Log::warning("SAML page requested, but samlData seems empty."); } } - \Log::warning("Something else went wrong while trying to login as SAML user"); } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index 95700e2992..1405a49b83 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -3,13 +3,11 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Http\Requests\SaveUserRequest; use App\Models\Setting; use App\Models\User; use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Http\Request; -use Illuminate\Validation\Rule; -use Illuminate\Validation\Validator; + class ResetPasswordController extends Controller { @@ -63,6 +61,14 @@ class ResetPasswordController extends Controller public function showResetForm(Request $request, $token = null) { + + $credentials = $request->only('email', 'token'); + + if (is_null($this->broker()->getUser($credentials))) { + \Log::debug('Password reset form FAILED - this token is not valid.'); + return redirect()->route('password.request')->with('error', trans('passwords.token')); + } + return view('auth.passwords.reset')->with( [ 'token' => $token, @@ -73,38 +79,53 @@ class ResetPasswordController extends Controller public function reset(Request $request) { + + $broker = $this->broker(); + $messages = [ 'password.not_in' => trans('validation.disallow_same_pwd_as_user_fields'), ]; $request->validate($this->rules(), $request->all(), $this->validationErrorMessages()); - // Check to see if the user even exists - $user = User::where('username', '=', $request->input('username'))->first(); + \Log::debug('Checking if '.$request->input('username').' exists'); + // Check to see if the user even exists - we'll treat the response the same to prevent user sniffing + if ($user = User::where('username', '=', $request->input('username'))->where('activated', '1')->whereNotNull('email')->first()) { + \Log::debug($user->username.' exists'); + + + // handle the password validation rules set by the admin settings + if (strpos(Setting::passwordComplexityRulesSaving('store'), 'disallow_same_pwd_as_user_fields') !== false) { + $request->validate( + [ + 'password' => 'required|notIn:["'.$user->email.'","'.$user->username.'","'.$user->first_name.'","'.$user->last_name.'"', + ], $messages); + } + + + // set the response + $response = $broker->reset( + $this->credentials($request), function ($user, $password) { + $this->resetPassword($user, $password); + }); + + // Check if the password reset above actually worked + if ($response == \Password::PASSWORD_RESET) { + \Log::debug('Password reset for '.$user->username.' worked'); + return redirect()->guest('login')->with('success', trans('passwords.reset')); + } + + \Log::debug('Password reset for '.$user->username.' FAILED - this user exists but the token is not valid'); + return redirect()->back()->withInput($request->only('email'))->with('error', trans('passwords.token')); - $broker = $this->broker(); - if (strpos(Setting::passwordComplexityRulesSaving('store'), 'disallow_same_pwd_as_user_fields') !== false) { - $request->validate( - [ - 'password' => 'required|notIn:["'.$user->email.'","'.$user->username.'","'.$user->first_name.'","'.$user->last_name.'"', - ], $messages); } - $response = $broker->reset( - $this->credentials($request), function ($user, $password) { - $this->resetPassword($user, $password); - } - ); - return $response == \Password::PASSWORD_RESET - ? $this->sendResetResponse($request, $response) - : $this->sendResetFailedResponse($request, $response); + \Log::debug('Password reset for '.$request->input('username').' FAILED - user does not exist or does not have an email address - but make it look like it succeeded'); + return redirect()->guest('login')->with('success', trans('passwords.reset')); + } - protected function sendResetFailedResponse(Request $request, $response) - { - return redirect()->back() - ->withInput(['username'=> $request->input('username')]) - ->withErrors(['username' => trans($response), 'password' => trans($response)]); - } + + } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 447f1bc300..90520738d0 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -75,12 +75,22 @@ class RouteServiceProvider extends ServiceProvider /** * Configure the rate limiters for the application. * + * https://laravel.com/docs/8.x/routing#rate-limiting + * * @return void */ protected function configureRateLimiting() { + + // Rate limiter for API calls RateLimiter::for('api', function (Request $request) { - return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()); + return Limit::perMinute(config('app.api_throttle_per_minute'))->by(optional($request->user())->id ?: $request->ip()); }); + + // Rate limiter for forgotten password requests + RateLimiter::for('forgotten_password', function (Request $request) { + return Limit::perMinute(config('auth.password_reset.max_attempts_per_min'))->by(optional($request->user())->id ?: $request->ip()); + }); + } } diff --git a/config/auth.php b/config/auth.php index 4fc626a964..7f580d68b1 100644 --- a/config/auth.php +++ b/config/auth.php @@ -98,14 +98,27 @@ return [ 'email' => 'auth.emails.password', 'table' => 'password_resets', 'expire' => env('RESET_PASSWORD_LINK_EXPIRES', 900), - 'throttle' => 60, + 'throttle' => [ 'max_attempts' => env('LOGIN_MAX_ATTEMPTS', 5), - 'lockout_duration' => env('LOGIN_LOCKOUT_DURATION', 60), + ] ], ], + /* + |-------------------------------------------------------------------------- + | Resetting Password Requests + |-------------------------------------------------------------------------- + | This sets the throttle for forgotten password requests + | + */ + 'password_reset' => [ + 'max_attempts_per_min' => env('PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN', 50), + ], + + + /* |-------------------------------------------------------------------------- | Password Confirmation Timeout @@ -117,6 +130,6 @@ return [ | */ - 'password_timeout' => 10800, + 'password_timeout' => env('PASSWORD_CONFIRM_TIMEOUT', 10800), ]; diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php index 4772940015..25633b4581 100644 --- a/resources/lang/en/passwords.php +++ b/resources/lang/en/passwords.php @@ -1,6 +1,8 @@ 'Success: If that email address exists in our system, a password recovery email has been sent.', - 'user' => 'No matching active user found with that email.', + 'sent' => 'If a matching user with a valid email address exists in our system, a password recovery email has been sent.', + 'user' => 'If a matching user with a valid email address exists in our system, a password recovery email has been sent.', + 'token' => 'This password reset token is invalid or expired, or does not match the username provided.', + 'reset' => 'Your password has been reset!', ]; diff --git a/resources/lang/en/reminders.php b/resources/lang/en/reminders.php index e7a476e3a2..8a197467df 100644 --- a/resources/lang/en/reminders.php +++ b/resources/lang/en/reminders.php @@ -14,11 +14,8 @@ return array( */ "password" => "Passwords must be six characters and match the confirmation.", - "user" => "Username or email address is incorrect", - - "token" => "This password reset token is invalid.", - - "sent" => "If a matching email address was found, a password reminder has been sent!", + "token" => 'This password reset token is invalid or expired, or does not match the username provided.', + 'sent' => 'If a matching user with a valid email address exists in our system, a password recovery email has been sent.', ); diff --git a/resources/views/layouts/basic.blade.php b/resources/views/layouts/basic.blade.php index 7efedf73b2..1a56a77b0a 100644 --- a/resources/views/layouts/basic.blade.php +++ b/resources/views/layouts/basic.blade.php @@ -56,7 +56,7 @@ @if (($snipeSettings) && ($snipeSettings->logo!=''))
- +
@endif diff --git a/routes/api.php b/routes/api.php index 921af2f1c3..29af435687 100644 --- a/routes/api.php +++ b/routes/api.php @@ -16,7 +16,7 @@ use Illuminate\Support\Facades\Route; | */ -Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:'.config('app.api_throttle_per_minute').',1']], function () { +Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], function () { Route::get('/', function () { diff --git a/routes/web.php b/routes/web.php index 63019632dd..7a6d8caa01 100644 --- a/routes/web.php +++ b/routes/web.php @@ -20,6 +20,8 @@ use App\Http\Controllers\StatuslabelsController; use App\Http\Controllers\SuppliersController; use App\Http\Controllers\ViewAssetsController; use App\Http\Controllers\Auth\LoginController; +use App\Http\Controllers\Auth\ForgotPasswordController; +use App\Http\Controllers\Auth\ResetPasswordController; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Auth; @@ -426,6 +428,39 @@ Route::group(['middleware' => 'web'], function () { [LoginController::class, 'postTwoFactorAuth'] ); + + + Route::post( + 'password/email', + [ForgotPasswordController::class, 'sendResetLinkEmail'] + )->name('password.email')->middleware('throttle:forgotten_password'); + + Route::get( + 'password/reset', + [ForgotPasswordController::class, 'showLinkRequestForm'] + )->name('password.request')->middleware('throttle:forgotten_password'); + + + Route::post( + 'password/reset', + [ResetPasswordController::class, 'reset'] + )->name('password.update')->middleware('throttle:forgotten_password'); + + Route::get( + 'password/reset/{token}', + [ResetPasswordController::class, 'showResetForm'] + )->name('password.reset'); + + + Route::post( + 'password/email', + [ForgotPasswordController::class, 'sendResetLinkEmail'] + )->name('password.email')->middleware('throttle:forgotten_password'); + + + + + Route::get( '/', [ @@ -446,7 +481,7 @@ Route::group(['middleware' => 'web'], function () { )->name('logout'); }); -Auth::routes(); +//Auth::routes(); Route::get( '/health',