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 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 #### #### 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 # The default authentication guard
# #
# Supported: # Supported:

9
Dockerfile vendored
View File

@ -156,7 +156,16 @@ ENV \
MAIL_ENCRYPTION=null \ MAIL_ENCRYPTION=null \
MAIL_FROM_NAME=null \ MAIL_FROM_NAME=null \
MAIL_FROM_ADDRESS=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 # 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 # The default authentication guard
# Supported: # Supported:
# 'web-guard' : The Laravel built-in auth system (default if nulled) # 'web-guard' : The Laravel built-in auth system (default if nulled)

View File

@ -27,6 +27,13 @@ class LoginController extends Controller
use AuthenticatesUsers; use AuthenticatesUsers;
/**
* The login throttle.
*
* @var integer
*/
protected $maxAttempts;
/** /**
* Handle a login request to the application. * 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())); 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 // 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 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. // the IP address of the client making these requests into this application.

View File

@ -20,6 +20,13 @@ class WebAuthnLoginController extends Controller
{ {
use AuthenticatesUsers; use AuthenticatesUsers;
/**
* The login throttle.
*
* @var integer
*/
protected $maxAttempts;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| WebAuthn Login Controller | 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())); 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 // 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 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. // 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() protected function configureRateLimiting()
{ {
RateLimiter::for('api', function (Request $request) { 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', 'repository' => 'https://github.com/Bubka/2FAuth',
'latestReleaseUrl' => 'https://api.github.com/repos/Bubka/2FAuth/releases/latest', 'latestReleaseUrl' => 'https://api.github.com/repos/Bubka/2FAuth/releases/latest',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| 2FAuth config | 2FAuth config
@ -29,6 +28,17 @@
'appSubdirectory' => env('APP_SUBDIRECTORY', ''), 'appSubdirectory' => env('APP_SUBDIRECTORY', ''),
], ],
/*
|--------------------------------------------------------------------------
| 2FAuth API config
|--------------------------------------------------------------------------
|
*/
'api' => [
'throttle' => env('THROTTLE_API', 60),
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| 2FAuth available translations | 2FAuth available translations

View File

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

View File

@ -54,7 +54,16 @@ services:
- MAIL_ENCRYPTION=null - MAIL_ENCRYPTION=null
- MAIL_FROM_NAME=null - MAIL_FROM_NAME=null
- MAIL_FROM_ADDRESS=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 # 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 # The default authentication guard
# Supported: # Supported:
# 'web-guard' : The Laravel built-in auth system (default if nulled) # '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 App\Models\User;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Tests\FeatureTestCase; 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() public function test_too_many_login_attempts_with_invalid_credentials_returns_too_many_request_error()
{ {
$throttle = 8;
Config::set('auth.throttle.login', $throttle);
$post = [ $post = [
'email' => $this->user->email, 'email' => $this->user->email,
'password' => self::WRONG_PASSWORD, '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);
$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() 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->user = User::factory()->create(['email' => self::EMAIL]);
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge( $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, 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);
$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);
} }
/** /**