I have a custom rate limiting rule in my RouteServiceProvider.php which looks like so;
protected function configureRateLimiting()
{
RateLimiter::for('example', function (Request $request) {
return Limit::perHour(5)->by(optional($request->user())->id ?: $request->ip())->response(function () {
return response()->view('auth.login', [
'error' =>
'You have exceeded the maximum number of login attempts. ' .
'Your account has been blocked for security reasons.',
'page' => 'login',
], 422);
});
});
}
This locks out the user after 5 attempts in an hour.
I would like to add a warning though after 2 attempts aswell, something like you have had two failed login attempts. If you continue entering an incorrect password your account will be locked.
I have tried the following in my login controller, but it doesnt work;
if (RateLimiter::remaining(optional($request->user())->id ?: $request->ip(), 2)) {
RateLimiter::hit(optional($request->user())->id ?: $request->ip());
return view('auth.login')->with([
'error' => 'You have had two failed login attempts. If you continue entering an incorrect password your account will be locked.',
'page' => 'login'
]);
}
Is this possible? I cant find anything regarding this.
Cheers,
the rate limiter information will be pass into the reponse headers X-RateLimit-Limit and X-RateLimit-Remaining which you may or may not be able to extract
It would be much easier to manually interact with RateLimiter class and manually increment the limiter, this way, you can return the remaining attempt and all the other information.
here's a basic example;
add the class use Illuminate\Support\Facades\RateLimiter;
then manually invoke hit and count the remaining attempts,
Route::get('/whatever-login-route', function( Request $request ) {
$key = 'login-limit:'.$request->ip;
//RateLimiter::resetAttempts( $key ); // resetting attempts
//RateLimiter::clear( $key ); // resetting attempts and lockout timer
return [
'hit' => RateLimiter::hit($key, 3600),
'remaining' => RateLimiter::remaining($key, 5),
'reset_at' => RateLimiter::availableIn($key)
];
});
This is just a basic example, but as you can see, in your login controller, you can pass the remaining or hit value and do your warning message after 2 hits, and return an error message with 429 header if the remaining value is less than 1 or hit value is more than 5.
Example usage in your case
$key = optional($request->user())->id ?: $request->ip();
$hit = RateLimiter::hit($key, 3600 ); // 2nd parameter is the value lockout timer in seconds
$remaining = RateLimiter::remaining($key, 5) // 2nd parameter is the number of allowed attempts in lockout define above
if ( $hit == 2 ) { // if ( $remaining == 3 )
return view('auth.login')->with([
'error' => 'You have had two failed login attempts. If you continue entering an incorrect password your account will be locked.',
'page' => 'login'
]);
}
Related
I have PHP 8.3, and Laravel 9 project.
I have a post route for updating the balance column value. And function in controller below
public function loadFunds(FundToCardRequest $request)
{
$user = auth()->user();
$request['clientUsername'] = 'username';
$request['username'] = $user->username;
$sum = $request['amount'];
$request['amount'] *= (1 - config('commissions.credit_card_from_wallet') / 100);
$response = SomeService::post('updateBalace', $request->toArray())->collect();
if ($response->get('code') == 200) {
DB::transaction(function () use ($user, $request, $sum) {
$balance = $user->wallets()->where('currency', 'USD')->first()->pivot->balance;
$user->wallets()->updateExistingPivot(1, ['balance' => $balance - $sum]);
$user->transactions()->create([
The function receives a custom request with the following rules.
public function rules()
{
$balance_usd = auth()->user()->wallets()->where('currency', 'USD')->first()->pivot->balance;
return [
'amount' => ['numeric', 'required', new NotZeroAmount(), new SendMoneyBalance($balance_usd)],
'cardId' => ['required'],
'ArrayHashId' => ['required'],
];
}
There is a rule SendMoneyBalance that checking is the current balance enough to send amount of money.
The problem is the following. Sometimes clients can send two simultaneous requests. The first request take time for processing after which the balance should be decreased and the final amount in the balance should be not enough. But the second request passes the rules because while first balance can't update. After this two requests balance goes to a negative value.
Are there any techniques to prevent this kind of simultaneous request? Or something like await structures like in other languages.
This is called a race condition and what you basically want to do is to create some sort or unique lock per request per user or your preference.
Example
Cache::lock('foo', 10)->block(5, function () {
// Lock acquired after waiting a maximum of 5 seconds...
});
See here for ref
First off let me know if this is not the correct place for this discussion!
We have decided to move our sessions to the Database layer in our application and handle the users state from there. One of the important issues that we hoped to resolve was user authentication to stop from brute forcing accounts by blocking login attempts from both the session layer (built from a database entry) as well as on the account layer - with an optional toggle to remove the account specific lockout for users as the potential remains for bad actors to then attempt to lock a legitimate user out of their account by spamming the account login - but the tradeoff here is then the account cannot be brute forced. This may be a toggle for specific accounts in the future if we decide the build the app around that idea more.
The current script for sessions includes a function (which i have included below) that you can call on the login event called check_login_attempts_exceeded and will return TRUE if they have been or FALSE if they have not been - Allowing you to either accept the login attempt, or block the attempt before it even hits the login server.
Basically, the user flow would work something like this:
Authenticated user::
We will just return FALSE right away because they are already logged in and don't
need to know this information on their own account. It does allow for them to check
other users with different user ids since they may be an admin and have a need for
that information? This is primarily designed for Guest logins anyways before the
user has been Authenticated in the system.
Spider::
Treat them as a guest account so the spider mechanism isn't exploited by people
to try and bypass the users login limit.
Guest account::
1) The system confirms they are a guest and continues, or returns FALSE if they
are already signed in to an account matching the ID of their user id -> this is
the Authenticated users section above.
2) Check if the user has a login lockout flag set on the SESSION. We don't care
about specific users **yet**, but we will check them afterwards.
NO::
1). Check if the user is at the max attempts in the system:
YES::
1). If the user is at the maximum attempts but doesn't have the flag set
for some reason, we add the login_lockout globally and add it to the
users SESSION in the database.
2). Return TRUE so the system knows they've exceeded max attempts
NO::
1). If the user has a supplied ID greater than 0, they can check if a
specific account is over. Since the flag is not yet set, this is the
only time this can be checked for specific accounts. This will be a
time code on the ACCOUNT level and if it is passed we handle it [1]
way, and if it's not passed we handle it [2] way.
[1]-> The lockout time on the ACCOUNT level is still in the future
so we are going to return TRUE because this account level check
shows the account is locked regardless of this SESSION and we
want to prevent brute forcing of accounts. The downside to this
method is a specific user can be locked out of their accounts
legitimately if a bad actor spams their login name.
[2]-> The lockout on the ACCOUNT level is in the past, so lets set
the login attempts and lockout time on the ACCOUNT level to 0
for both and return FALSE since the timeout is no longer on
and the account hasn't exceeded it's limit! :)
2). The user id wasn't supplied so we assume the user hasn't gone over
yet because the flag isn't set yet by any previous condition and the
lockout time doesn't exist either so we return FALSE :)
2). Return FALSE as a failsafe incase the user id wasn't set and they are
not at the limit meaning they've passed the test and not exceeded attempts
YES::
1). The user has the flag set so lets see if the timeout on the flag has
expired and remove it if it has
YES::
1). Remove the flag, update the session and reset the attempts and the
lockout time to 0 again. Ideally return FALSE now, but we need to
check the ACCOUNT specific timeout as well just in case that one is
still set.
The last portion is the bit I am wanting to be sure of. Basically, if the user HAS a time flag on their SESSION, we need to check if its expired and remove it if it is - but then the ACCOUNT layer inside of this may be time locked separately so we need to check that, remove it if its expired, or return as invalid if its not expired.
I am doing this with the following code (PHP) and have commented as much of the code as possible to explain the steps and where I am at:
// Check if the user has exceeded their login attempts, and if they are locked
// out return TRUE, and if the lockout has expired, remove it from the session
// AND the user account if it is set - Optional return time to unlock
function check_login_attempts_exceeded($database, $config, $userid = 0, $return_time = FALSE) {
// Set the time of now for checking if they are past the lockout expiry
$time = CURRENT_TIME;
$userid = (int) $userid;
$unauthenticated_user = FALSE;
// Check if this user is logged in - If they are, we can return false
// since this user is already logged in and doesn't need to have attempts
// logged for them anymore.
if ($userid > 0 && $userid == $this->userid) {
return FALSE;
} else {
$unauthenticated_user = TRUE;
}
// If the user is a guest, we can process the request since it's not important
// otherwise. This condition should always be true, but better safe than sorry?
if ($unauthenticated_user) {
// If the login_timeout isn't set we can check if the attempts are over
// and set it otherwise, it must be set so we will see if its expired
if (!$this->login_timeout || $this->login_timeout == 0) {
// Check the global lockout first - if it above the max then we will
// ignore the fact that the user is over since we're not there yet
if ($this->login_attempts >= $config->general->max_login_trys) {
$this->login_timeout = (int) $time + ($config->general->lockout_time * 60);
$database->update_prepared_query("sessions", array("lockout_time" => $this->login_timeout), array("sid" => $this->session_id));
return TRUE;
}
// Now lets check the specific user case, because we know that the
// above has been handled and returned already
if ($userid > 0) {
$user_info = $database->prepared_select("users", "WHERE uid = ? LIMIT 1", array($userid), "loginattempts, loginlockoutexpiry");
if ($user_info) {
$user_specific_loginattempts = $user_info["loginattempts"];
$user_specific_loginexpiry = $user_info["loginlockoutexpiry"];
// If this specific user is over the max attempts, then we return
// true because they're over the max attempt
if ($user_specific_loginattempts >= $config->general->max_login_trys) {
// We also want to see if their expiry is over the max limit
// and it if is, we will return true, if it is expired,
// lets remove it and set these back to 0 and return false!
if ($user_specific_loginexpiry > $time) {
if ($return_time) {
$secsleft = (int) ($user_specific_loginexpiry - $time);
$hoursleft = floor($secsleft / 3600);
$minsleft = floor(($secsleft / 60) % 60);
$secsleft = floor($secsleft % 60);
return array("hours" => $hoursleft, "minutes" => $minsleft, "seconds" => $secsleft);
}
return TRUE;
} else {
// This user specific timeout has expired, so lets
// remove it from the system and let the user attempt
// a login!
$database->update_prepared_query("users", array("loginattempts" => 0, "loginlockoutexpiry" => 0), array("uid" => $userid));
return FALSE;
}
}
} else {
// There was no user information found for this id, so we can't
// return a result and we will just return false instead
return FALSE;
}
}
// Must not be over yet then, so lets return false as no user was
// supplied and the user hasn't hit the limit globally yet either
return FALSE;
} else {
// The login timeout is set, let see if it's expired or not?
if ($this->login_timeout <= CURRENT_TIME) {
if ($userid > 0) {
$user_info = $database->prepared_select("users", "WHERE uid = ? LIMIT 1", array($userid), "loginattempts, loginlockoutexpiry");
if ($user_info) {
if ($user_info["loginattempts"] >= $config->general->max_login_trys) {
if ($user_info["loginlockoutexpiry"] > $time) {
return TRUE;
} else {
$this->login_timeout = 0;
$this->login_attempts = 0;
$database->update_prepared_query("sessions", array("login_attempts" => $this->login_attempts, "lockout_time" => $this->login_timeout), array("sid" => $this->session_id));
$database->update_prepared_query("users", array("loginattempts" => 0, "loginlockoutexpiry" => 0), array("uid" => $userid));
return FALSE;
}
} else {
// The user they're trying for is not at the max tries so we can return false!
return FALSE;
}
} else {
// There was no user information found for this id, so we can't
// return a result and we will just return false instead
return FALSE;
}
} else {
// The login timeout has expired for this guest account so
// we remove it globally!
$this->login_timeout = 0;
$this->login_attempts = 0;
$database->update_prepared_query("sessions", array("login_attempts" => $this->login_attempts, "lockout_time" => $this->login_timeout), array("sid" => $this->session_id));
return FALSE;
}
}
// They must still be expired, so lets return true!
return TRUE;
}
}
return FALSE;
}
Does this seem like a good userflow and does the function seem to be missing anything that you would suggest that could potentially lead to an error?
I have been troubleshooting it and all my conditions SEEM to be passing, but I've also spent about 100 hours on this application already and a fresh pair of eyes to suggest any holes I may have missed would be very beneficial at this point.
If it all looks good, let me know so I can stop going crazy overbuilding this thing!
i just force expire the cookie it's quite simple its balance between being secure and provably annoying for brute force
<?php
session_start();
session_regenerate_id(true); // regenerate new cookie everytime page refresh
$reload_userTimeout=40; // set the new cookie to expire 40seconds everytime user reload the page( they need to type the form in just 40 secs)
setcookie(session_name(),session_id(),time()+$reload_userTimeout);
if (login detail wrong) {
session_unset();
session_regenerate_id(true); // regenerate new cookie
$captchaWrong_userTimeout=15; // since they got login details wrong The cookie would now expire 15 secs ( shorter than before so they need to type fast as possible :p )
setcookie(session_name(),session_id(),time()+$captchaWrong_userTimeout);
} else { // login detail is correct
session_unset();
session_regenerate_id(true); // regenerate new cookie
$captchaCorrect_userTimeout=1; // force expire the cookie in just 1 second
setcookie(session_name(),session_id(),time()+$captchaCorrect_userTimeout);
session_write_close(); // prevent cookie from being overwritten ( user must clear web browser cookies or use incognito mode to overwrite the cookie )
}
?>
so i am working at voting system that have code like this
public function storeVote(Request $request)
{
$voting = Voting::findOrFail($request->voting_id);
if($voting->status == 1){
$checkVote = vote::where('voting_id',$request->voting_id)->where('name',$request->name)->where('voting_candidate_id',null)->first();
if($checkVote){
\DB::beginTransaction();
try{
$candidate = candidate::findOrFail($request->voting_candidate_id);
$skor = $candidate->skor + 1;
$candidate->skor = $skor;
$candidate->update();
$checkVote->voting_candidate_id = $request->voting_candidate_id;
$checkVote->update();
$vote_ok = $voting->vote_ok + 1;
$voting->vote_ok = $vote_ok;
$voting->update();
event(new VotingEvent($skor, $voting->id, $candidate->id));
CandidateProfile::flushCache();
\DB::commit();
return response()
->json([
'saved' => true,
'message' => 'Voting done.',
]);
} catch (\Exception $e){
\DB::rollBack();
abort(500, $e->getMessage());
}
}else{
return response()
->json([
'saved' => false,
'message' => 'sorry, you already vote'
]);
}
}else{
return response()
->json([
'saved' => false,
'message' => 'Sorry, Voting session not started yet'
]);
}
}
so this function act as a way for user to vote, the participant have a unique link where they only need to choose the candidate and then it will be trigger the function above
the problem is when i tested to do like 30 vote at the same time, half of them not saved.
any idea why?
update:
the data that are not saved:
candidate skor is not updated or not multiplied
voting information about vote_ok which mean total vote that being use
Note there is a catch when you use update queries. For eg: in you above code you are updating the candicate_skor using;
$skor = $candidate->skor + 1;
$candidate->skor = $skor;
$candidate->update();
The problem arises when your server gets multiple concurrent requests for the same route. For each of the requests (let's say you have 5 requests) the function retrieves the old candidate_skore value let's say it was equal to 1. Now when each of them updates the value DB value it will be equal to 2. Even though you have 5 upvote requests that should update the DB value to 6 it updates to just 2, causing you to lose 4 votes.
Ideally, you should keep a relation table for all the votes received and only insert it into that relation table. That way even if simultaneous requests are served all of them will insert new entries to the table. Finally, your total vote should be equal to the count of all rows in that relation table.
how can I set time interval or time difference between the first time the user requested for the verification code and the second try which should be 30 seconds?
also how to display the time counter: 29:00 down to 0 seconds?
public function sendSms($request)
{
$apiKey = config('services.smsapi.ApiKey');
$client = new \GuzzleHttp\Client();
$endpoint = "https://www.sms123.net/api/send.php";
try
{
$response = $client->request('GET', $endpoint, ['query' => [
'recipients' => $request->contact_number,
'apiKey' => $apiKey,
'messageContent'=>'testSite.com verification code is '.$request->code,
]]);
$statusCode = $response->getStatusCode();
$content = $response->getBody();
$content = json_decode($response->getBody(), true);
return $content['msgCode'];
}
catch (Exception $e)
{
echo "Error: " . $e->getMessage();
}
}
Thankfully, Laravel gets you covered in this aspect. In Laravel, you can achieve rate-limiting using a middleware called throttle which comes out of the box in Laravel. You need to assign this throttle middleware to the route or group of routes.
The middleware basically accepts two parameters, specifically “number of requests” and “duration of time”, which determines the maximum number of requests that can be made in a given number of minutes.
Basic example
You can assign a throttle middleware to a single route like below
Route::get('admin/profile', function () {
//
})->middleware('auth', 'throttle:30,1');
As you can see, the above route configuration will allow an authenticated user access route 30 times per minute. If user exceed this limit within the specified time span, Laravel will return a 429 Too Many Requests with following response headers.
x-ratelimit-limit: 2
x-ratelimit-remaining: 0
x-ratelimit-reset: 1566834663
Then with vue or js on your frontend you can make a counter that will start counting the desired number so that the user knows how much time he has left.
This question has been asked many times, but decent solution couldn't be found.
I do this - $newToken = auth()->refresh();
in my custom claims, I have
public function getJWTCustomClaims()
{
return [
'is_verified' => $this->verified,
'email' => $this->email,
'role' => $this->getMainRole()->name
];
}
Scenario - first, when I login, it returns me the jwt token. in that jwt token, I have is_verified , email, role set. Let's say is_verified was 0 at the time i got the token. Now, I changed it to 1 in database. NOw when I refrehs the token, as I showed you above, returned jwt token still has is_verified equal to 0, but it should have 1. Any idea how to fix it?
Try with $newToken = auth()->refresh(false, true);
Second parameter is "reset claims":
JWTGuard class -
public function refresh($forceForever = false, $resetClaims = false)