Skip to content

Commit 566eb98

Browse files
authored
Add rate limit (#128)
* add rate limit * add none to disable rate limit * add env MAGICLINK_RATE_LIMIT * add docs
1 parent 330e0a1 commit 566eb98

File tree

7 files changed

+146
-37
lines changed

7 files changed

+146
-37
lines changed

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ offer secure content and even log in to the application.
2727
- [Lifetime](#lifetime)
2828
- [Events](#events)
2929
- [Customization](#customization)
30+
- [Rate limiting](#rate-limiting)
31+
- [Testing](#testing)
32+
- [Contributing](#contributing)
33+
- [Security](#security)
3034

3135
## Installation
3236

@@ -326,8 +330,8 @@ php artisan vendor:publish --provider="MagicLink\MagicLinkServiceProvider" --tag
326330

327331
And edit the file `config/magiclink.php`
328332

329-
330333
### Migrations
334+
331335
To customize the migration files of this package you need to publish the migration files:
332336

333337
```bash
@@ -336,7 +340,6 @@ php artisan vendor:publish --provider="MagicLink\MagicLinkServiceProvider" --tag
336340

337341
You'll find the published files in `database/migrations/*`
338342

339-
340343
### Custom response when magiclink is invalid
341344

342345
When the magicLink is invalid by default the http request return a status 403.
@@ -407,6 +410,19 @@ return a `view()`
407410
],
408411
```
409412

413+
## Rate limiting
414+
415+
You can limit the number of requests per minute for a magic link. To do this, you need to
416+
set the `MAGICLINK_RATE_LIMIT` environment variable to the desired value.
417+
418+
By default, the rate limit is 100 attempts per minutes. Use `none` to disable the rate limit.
419+
420+
```bash
421+
# .env
422+
423+
MAGICLINK_RATE_LIMIT='none'
424+
```
425+
410426
## Testing
411427

412428
Run the tests with:

config/magiclink.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
<?php
22

3+
34
return [
45

56
'access_code' => [
7+
/*
8+
|--------------------------------------------------------------------------
9+
| Access Code View
10+
|--------------------------------------------------------------------------
11+
|
12+
| Here you may specify the view to ask for access code.
13+
|
14+
*/
615
'view' => 'magiclink::ask-for-access-code-form',
716
],
817

@@ -32,7 +41,24 @@
3241
'class' => MagicLink\Responses\Response::class,
3342
],
3443

35-
'token' => [
44+
'middlewares' => [
45+
'throttle:magiclink',
46+
MagicLink\Middlewares\MagiclinkMiddleware::class,
47+
'web',
48+
],
49+
50+
/*
51+
|--------------------------------------------------------------------------
52+
| Rate Limit
53+
|--------------------------------------------------------------------------
54+
|
55+
| Here you may specify the number of attempts to rate limit per minutes
56+
|
57+
| Default: 100, if you want to disable rate limit, set as 'none'
58+
*/
59+
'rate_limit' => env('MAGICLINK_RATE_LIMIT', 100),
60+
61+
'token' => [
3662
/*
3763
|--------------------------------------------------------------------------
3864
| Token size

routes/routes.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
<?php
22

33
use Illuminate\Support\Facades\Route;
4-
use MagicLink\Middlewares\MagiclinkMiddleware;
54

65
Route::group(
76
[
8-
'middleware' => [
9-
MagiclinkMiddleware::class,
10-
'web',
11-
],
7+
'middleware' => config('magiclink.middlewares'),
128
],
139
function () {
1410
Route::get(

src/MagicLinkServiceProvider.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace MagicLink;
44

5+
use Illuminate\Cache\RateLimiting\Limit;
6+
use Illuminate\Support\Facades\RateLimiter;
57
use Illuminate\Support\ServiceProvider;
68

79
class MagicLinkServiceProvider extends ServiceProvider
@@ -15,11 +17,25 @@ public function boot()
1517
{
1618
$this->offerPublishing();
1719

20+
$this->registerRateLimit();
21+
1822
$this->loadRouteMagicLink();
1923

2024
$this->loadViewMagicLink();
2125
}
2226

27+
private function registerRateLimit(): void
28+
{
29+
$rateLimit = config('magiclink.rate_limit', 100);
30+
31+
RateLimiter::for(
32+
'magiclink',
33+
fn () => $rateLimit === 'none'
34+
? Limit::none()
35+
: Limit::perMinute($rateLimit)
36+
);
37+
}
38+
2339
private function loadRouteMagicLink(): void
2440
{
2541
$disableRegisterRoute = config('magiclink.disable_default_route', false);

tests/Http/HttpHeadTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace MagicLink\Test\Http;
4+
5+
use MagicLink\Actions\ResponseAction;
6+
use MagicLink\MagicLink;
7+
use MagicLink\Test\TestCase;
8+
9+
class HttpHeadTest extends TestCase
10+
{
11+
public function test_http_head_request_has_not_effects()
12+
{
13+
$magiclink = MagicLink::create(new ResponseAction(function () {
14+
return 'private content';
15+
}));
16+
17+
$magiclink->num_visits = 4;
18+
$magiclink->save();
19+
20+
$this->head($magiclink->url)
21+
->assertStatus(200)
22+
->assertDontSeeText('private content');
23+
24+
$magiclink->refresh();
25+
26+
$this->assertEquals(4, $magiclink->num_visits);
27+
}
28+
29+
public function test_http_head_request_without_valid_magiclink()
30+
{
31+
$magiclink = MagicLink::create(new ResponseAction(function () {
32+
return 'private content';
33+
}));
34+
35+
$this->head($magiclink->url . '-bad')
36+
->assertStatus(404);
37+
}
38+
}

tests/HttpTest.php renamed to tests/Http/HttpTest.php

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<?php
22

3-
namespace MagicLink\Test;
3+
namespace MagicLink\Test\Http;
44

55
use MagicLink\Actions\ResponseAction;
66
use MagicLink\MagicLink;
7+
use MagicLink\Test\TestCase;
78

89
class HttpTest extends TestCase
910
{
@@ -25,24 +26,6 @@ public function test_http_get_request()
2526
$this->assertEquals(5, $magiclink->num_visits);
2627
}
2728

28-
public function test_http_head_request_has_not_effects()
29-
{
30-
$magiclink = MagicLink::create(new ResponseAction(function () {
31-
return 'private content';
32-
}));
33-
34-
$magiclink->num_visits = 4;
35-
$magiclink->save();
36-
37-
$this->head($magiclink->url)
38-
->assertStatus(200)
39-
->assertDontSeeText('private content');
40-
41-
$magiclink->refresh();
42-
43-
$this->assertEquals(4, $magiclink->num_visits);
44-
}
45-
4629
public function test_http_options_request_has_not_effects()
4730
{
4831
$magiclink = MagicLink::create(new ResponseAction(function () {
@@ -73,14 +56,4 @@ public function test_http_urlencode_legacy()
7356
->assertStatus(200)
7457
->assertSeeText('private content');
7558
}
76-
77-
public function test_http_head_request_without_valid_magiclink()
78-
{
79-
$magiclink = MagicLink::create(new ResponseAction(function () {
80-
return 'private content';
81-
}));
82-
83-
$this->head($magiclink->url . '-bad')
84-
->assertStatus(404);
85-
}
8659
}

tests/Http/HttpThrottleTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace MagicLink\Test\Http;
4+
5+
use MagicLink\Actions\ResponseAction;
6+
use MagicLink\MagicLink;
7+
use MagicLink\MagicLinkServiceProvider;
8+
use MagicLink\Test\TestCase;
9+
10+
class HttpThrottleTest extends TestCase
11+
{
12+
public function test_http_failed_when_rate_limit_is_exceeded()
13+
{
14+
config(['magiclink.rate_limit' => 1]);
15+
(new MagicLinkServiceProvider($this->app))->boot();
16+
17+
$magiclink = MagicLink::create(new ResponseAction(function () {
18+
return 'private content';
19+
}));
20+
21+
$this->get($magiclink->url)
22+
->assertStatus(200);
23+
24+
$this->get($magiclink->url)
25+
26+
->assertStatus(429);
27+
}
28+
public function test_http_when_rate_limit_is_none()
29+
{
30+
config(['magiclink.rate_limit' => 'none']);
31+
(new MagicLinkServiceProvider($this->app))->boot();
32+
33+
$magiclink = MagicLink::create(new ResponseAction(function () {
34+
return 'private content';
35+
}));
36+
37+
$this->get($magiclink->url)
38+
->assertStatus(200);
39+
40+
$this->get($magiclink->url)
41+
->assertStatus(200);
42+
}
43+
44+
}

0 commit comments

Comments
 (0)