Pārlūkot izejas kodu

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

Bubka 2 gadi atpakaļ
vecāks
revīzija
140cc70cef

+ 17 - 0
.env.example

@@ -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 - 0
Dockerfile

@@ -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)

+ 9 - 0
app/Http/Controllers/Auth/LoginController.php

@@ -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 @@ class LoginController extends Controller
     {
         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.

+ 9 - 0
app/Http/Controllers/Auth/WebAuthnLoginController.php

@@ -20,6 +20,13 @@ class WebAuthnLoginController extends Controller
 {
     use AuthenticatesUsers;
 
+    /**
+     * The login throttle.
+     *
+     * @var integer
+     */
+    protected $maxAttempts;
+
     /*
     |--------------------------------------------------------------------------
     | WebAuthn Login Controller
@@ -67,6 +74,8 @@ class WebAuthnLoginController extends Controller
     {
         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.

+ 2 - 1
app/Providers/RouteServiceProvider.php

@@ -73,7 +73,8 @@ class RouteServiceProvider extends ServiceProvider
     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());
         });
     }
 }

+ 11 - 1
config/2fauth.php

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

+ 4 - 0
config/auth.php

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

+ 9 - 0
docker/docker-compose.yml

@@ -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)

+ 39 - 0
tests/Api/v1/ThrottlingTest.php

@@ -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);
+    }
+}

+ 12 - 7
tests/Feature/Http/Auth/LoginTest.php

@@ -4,6 +4,7 @@ namespace Tests\Feature\Http\Auth;
 
 use App\Models\User;
 use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Config;
 use Tests\FeatureTestCase;
 
 /**
@@ -140,19 +141,23 @@ class LoginTest extends FeatureTestCase
      */
     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,
         ];
 
-        $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);
+        for ($i=0; $i < $throttle - 1; $i++) {
+            $this->json('POST', '/user/login', $post);
+        }
 
-        $response->assertStatus(429);
+        $this->json('POST', '/user/login', $post)
+            ->assertUnauthorized();
+
+            $this->json('POST', '/user/login', $post)
+        ->assertStatus(429);
     }
 
     /**

+ 11 - 7
tests/Feature/Http/Auth/WebAuthnLoginControllerTest.php

@@ -278,6 +278,9 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
      */
     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 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
             false,
         )]);
 
-        $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);
+        for ($i=0; $i < $throttle - 1; $i++) {
+            $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);
     }
 
     /**