Make Login & API throttling editable using the .env file - Close #163

This commit is contained in:
Bubka 2023-03-16 15:57:22 +01:00
parent 9913560787
commit 140cc70cef
11 changed files with 132 additions and 16 deletions

View File

@ -103,8 +103,25 @@ MAIL_FROM_NAME=null
MAIL_FROM_ADDRESS=null
#### API settings ####
# The maximum number of API calls in a minute from the same IP.
# Once reached, all requests from this IP will be rejected until the minute has elapsed.
#
# Set to null to disable the API throttling.
THROTTLE_API=60
#### Authentication settings ####
# The number of times per minute a user can fail to log in before being locked out.
# Once reached, all login attempts will be rejected until the minute has elapsed.
#
# This setting applies to both email/password and webauthn login attemps.
LOGIN_THROTTLE=5
# The default authentication guard
#
# Supported:

9
Dockerfile vendored
View File

@ -156,7 +156,16 @@ ENV \
MAIL_ENCRYPTION=null \
MAIL_FROM_NAME=null \
MAIL_FROM_ADDRESS=null \
# API settings
# The maximum number of API calls in a minute from the same IP.
# Once reached, all requests from this IP will be rejected until the minute has elapsed.
# Set to null to disable the API throttling.
THROTTLE_API=60 \
# Authentication settings
# The number of times per minute a user can fail to log in before being locked out.
# Once reached, all login attempts will be rejected until the minute has elapsed.
# This setting applies to both email/password and webauthn login attemps.
LOGIN_THROTTLE=5 \
# The default authentication guard
# Supported:
# 'web-guard' : The Laravel built-in auth system (default if nulled)

View File

@ -27,6 +27,13 @@ class LoginController extends Controller
use AuthenticatesUsers;
/**
* The login throttle.
*
* @var integer
*/
protected $maxAttempts;
/**
* Handle a login request to the application.
*
@ -39,6 +46,8 @@ public function login(LoginRequest $request)
{
Log::info(sprintf('User login requested by %s from %s', var_export($request['email'], true), $request->ip()));
$this->maxAttempts = config('auth.throttle.login');
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.

View File

@ -20,6 +20,13 @@ class WebAuthnLoginController extends Controller
{
use AuthenticatesUsers;
/**
* The login throttle.
*
* @var integer
*/
protected $maxAttempts;
/*
|--------------------------------------------------------------------------
| WebAuthn Login Controller
@ -67,6 +74,8 @@ public function login(WebauthnAssertedRequest $request)
{
Log::info(sprintf('User login via webauthn requested by %s from %s', var_export($request['email'], true), $request->ip()));
$this->maxAttempts = config('auth.throttle.login');
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.

View File

@ -73,7 +73,8 @@ private function getApiNamespace(string $version)
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->ip());
$maxAttempts = config('2fauth.api.throttle');
return is_null($maxAttempts) ? Limit::none() : Limit::perMinute($maxAttempts)->by($request->ip());
});
}
}

View File

@ -13,7 +13,6 @@
'repository' => 'https://github.com/Bubka/2FAuth',
'latestReleaseUrl' => 'https://api.github.com/repos/Bubka/2FAuth/releases/latest',
/*
|--------------------------------------------------------------------------
| 2FAuth config
@ -29,6 +28,17 @@
'appSubdirectory' => env('APP_SUBDIRECTORY', ''),
],
/*
|--------------------------------------------------------------------------
| 2FAuth API config
|--------------------------------------------------------------------------
|
*/
'api' => [
'throttle' => env('THROTTLE_API', 60),
],
/*
|--------------------------------------------------------------------------
| 2FAuth available translations

View File

@ -2,6 +2,10 @@
return [
'throttle' => [
'login' => env('LOGIN_THROTTLE', 5),
],
/*
|--------------------------------------------------------------------------
| Authentication Defaults

View File

@ -54,7 +54,16 @@ services:
- MAIL_ENCRYPTION=null
- MAIL_FROM_NAME=null
- MAIL_FROM_ADDRESS=null
# API settings
# The maximum number of API calls in a minute from the same IP.
# Once reached, all requests from this IP will be rejected until the minute has elapsed.
# Set to null to disable the API throttling.
- THROTTLE_API=60
# Authentication settings
# The number of times per minute a user can fail to log in before being locked out.
# Once reached, all login attempts will be rejected until the minute has elapsed.
# This setting applies to both email/password and webauthn login attemps.
- LOGIN_THROTTLE=5
# The default authentication guard
# Supported:
# 'web-guard' : The Laravel built-in auth system (default if nulled)

View File

@ -0,0 +1,39 @@
<?php
namespace Tests\Unit\Api\v1\Controllers;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Tests\FeatureTestCase;
/**
* @covers \App\Providers\RouteServiceProvider
*/
class ThrottlingTest extends FeatureTestCase
{
/**
* @test
*/
public function test_api_calls_are_throttled_using_config()
{
/**
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
*/
$user = User::factory()->create();
$throttle = 5;
Config::set('2fauth.api.throttle', $throttle);
$this->actingAs($user, 'api-guard');
for ($i=0; $i < $throttle - 1; $i++) {
$this->json('GET', '/api/v1/twofaccounts/count');
}
$this->json('GET', '/api/v1/twofaccounts/count')
->assertOk();
$this->json('GET', '/api/v1/twofaccounts/count')
->assertStatus(429);
}
}

View File

@ -4,6 +4,7 @@
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Tests\FeatureTestCase;
/**
@ -140,19 +141,23 @@ public function test_user_login_with_invalid_credentials_returns_unauthorized()
*/
public function test_too_many_login_attempts_with_invalid_credentials_returns_too_many_request_error()
{
$throttle = 8;
Config::set('auth.throttle.login', $throttle);
$post = [
'email' => $this->user->email,
'password' => self::WRONG_PASSWORD,
];
for ($i=0; $i < $throttle - 1; $i++) {
$this->json('POST', '/user/login', $post);
$this->json('POST', '/user/login', $post);
$this->json('POST', '/user/login', $post);
$this->json('POST', '/user/login', $post);
$this->json('POST', '/user/login', $post);
$response = $this->json('POST', '/user/login', $post);
}
$response->assertStatus(429);
$this->json('POST', '/user/login', $post)
->assertUnauthorized();
$this->json('POST', '/user/login', $post)
->assertStatus(429);
}
/**

View File

@ -278,6 +278,9 @@ public function test_webauthn_invalid_login_returns_unauthorized()
*/
public function test_too_many_invalid_login_attempts_returns_too_many_request_error()
{
$throttle = 8;
Config::set('auth.throttle.login', $throttle);
$this->user = User::factory()->create(['email' => self::EMAIL]);
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
@ -286,14 +289,15 @@ public function test_too_many_invalid_login_attempts_returns_too_many_request_er
false,
)]);
for ($i=0; $i < $throttle - 1; $i++) {
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
$response = $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
}
$response->assertStatus(429);
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
->assertUnauthorized();
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
->assertStatus(429);
}
/**