Benson Macharia
Posted on March 15, 2024
What is Rate Limiting?
Rate limiting is a technique used in web and API development to control the rate of traffic sent or received by a server.
Why Rate Limiting?
Rate limiting is a crucial aspect of modern application security, offering a powerful defense against various types of attacks such as brute force, DDoS, and API abuse. By restricting the number of requests a user or IP address can make within a certain timeframe, rate limiting helps prevent server overload, maintains application performance, and enhances overall security posture.
Implementation
In this article, we'll delve into the concept of rate limiting in Laravel; a popular PHP framework. We will explore how to set it up, customize it to suit your application's needs, and handle common scenarios. By the end, you'll have the knowledge and confidence to implement rate limiting in your Laravel applications, enhancing their security and stability.
Design
In Laravel, requests are routed through the router service, which directs them to the appropriate middleware for processing, then to controllers for handling business logic, models for interacting with the database, and finally to views for rendering the response back to the client.
Let's Set Up
You can download the complete source code here or you can follow this Laravel guide to scaffold your application as follows.
$ composer create-project laravel/laravel:^10.0 laravel-rate-limiting
$ cd laravel-rate-limiting
$ php artisan serve
Remember to edit your .env
file and then run
$ php artisan migrate
We will additionally create:-
- Three controllers i.e.
LoginRegisterController.php
,APIController.php
andProductController.php
under app/Http/Controllers - Two Models i.e.
User.php
andProduct.php
under app/Models. - Auth and Product views under resources/views
- Migrations under database/migrations
Rate Limiting Implementation
To implement rate limiting, we will make use of the rate limiting capability provided with Laravel Middleware to throttle incoming HTTP requests before they reach our controllers.
To do that, we will modify the App\Providers\RouteServiceProvider.php
to define the rate limiter configurations to suit our application needs.
<?php
namespace App\Providers;
class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Modify rate limiting for APIs to 60 requests per minute
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Rate limit auth endpoints to 10 requests per minute by user IP
RateLimiter::for('auth', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
// Rate limit products endpoints to 30 requests per minute by user ID
RateLimiter::for('product', function (Request $request) {
return Limit::perMinute(30)->by($request->user()?->id);
});
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}
In the above example, we have defined two rate limiters, one called auth
that limits access to the auth routes to 10 requests per minute by user IP and another one called product
that limits access to products endpoints to 30 requests per minute by user ID. We have also modified the rate limiter for APIs to 60 requests per minute by user IP or ID.
We can now apply the rate limiters to the routes we want to throttle using the middleware as follows:
routes/api.php
// Endpoint requires authentication and is throttled through the auth rate limiter
Route::middleware(['auth:api','throttle:auth'])->get('/profile', function (Request $request) {
return $request->user();
});
// Endpoints do not require authentication but they are throttled through the auth rate limiter
Route::middleware(['throttle:auth'])->post('/user/register', [APIController::class, 'register']);
Route::middleware(['throttle:auth'])->post('/user/login', [APIController::class, 'login']);
// Endpoints require authentication and are throttled through the auth rate limiter
Route::middleware(['auth:api','throttle:auth'])->get('/users', [APIController::class, 'users']);
Route::middleware(['auth:api','throttle:auth'])->get('/user/{id}', [APIController::class, 'user']);
// Endpoints require authentication and are throttled through the auth product limiter
Route::middleware(['auth:api','throttle:product'])->post('/product', [APIController::class, 'newProduct']);
Route::middleware(['auth:api','throttle:product'])->get('/products', [APIController::class, 'index']);
Route::middleware(['auth:api','throttle:product'])->get('/product/{id}', [APIController::class, 'product']);
routes/web.php
// Not throttled
Route::get('/', function () {
return view('welcome');
});
// Throttled through the auth rate limiter
Route::group(['middleware' => 'throttle:auth'], function () {
Route::get('/register', [LoginRegisterController::class, 'register'])->name('register');
Route::post('/store', [LoginRegisterController::class, 'store'])->name('store');
Route::get('/login', [LoginRegisterController::class, 'login'])->name('login');
Route::post('/authenticate', [LoginRegisterController::class, 'authenticate'])->name('authenticate');
Route::get('/dashboard', [LoginRegisterController::class, 'dashboard'])->name('dashboard');
Route::post('/logout', [LoginRegisterController::class, 'logout'])->name('logout');
});
// Throttled through the product rate limiter
Route::middleware(['middleware' => 'throttle:product'])->group(function () {
Route::get('/products', [ProductController::class, 'index'])->name('products');
Route::get('/product/{product}', [ProductController::class, 'show'])->name('product.show');
Route::post('/product', [ProductController::class, 'store'])->name('product.store');
});
Testing Rate Limiting
Moment on truth.!
Run
php artisan serve
1. API
We have eight API endpoints as below
- User register ```
$ curl -X POST --header "Content-Type: application/json" --data '{"name":"Jane Doe", "email":"janedoe@example.com", "password":"pAssW0rd@s3cr3t", "password_confirmation":"pAssW0rd@s3cr3t"}' http://localhost:8000/api/user/register
HTTP/1.1 201 Created
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
{"message":"User registered successfully","user":{"name":"John Doe","email":"johndoe@example.com","api_token":"hSAx1XBcpFDNUNFqCQvQ9OOgGfBF6t5rzxJicEsjsh0o4HGmaBCVhA7tOEpp","updated_at":"2024-03-14T23:30:45.000000Z","created_at":"2024-03-14T23:30:45.000000Z","id":9}}
On successful user registration we can see the response above. Note the `X-RateLimit-Limit` and `X-RateLimit-Remaining` values meaning we have nine more requests remaining allowed before we get blocked. We can simulate this using a tool such as [Burp suite](https://portswigger.net/burp/communitydownload) repeater or intruder as illustrated in the image below.
![Error Too May Requests API](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c3nq36qvh7b11oragahb.png)
After sending 10 requests, we will be blocked on the 11th one as per the configuration we set in the API auth limiter.
Try the other endpoints as below.
- User login
$ curl -X POST --header "Content-Type: application/json" --data '{"email":"janedoe@example.com", "password":"pAssW0rd@s3cr3t"}' http://localhost:8000/api/user/login
- List all users
$ curl -X GET --header "Content-Type: application/json" --header "Authorization: Bearer z7ZOw6IqUbDHLdozERIwp4w9Ti8wfvaI51b8xTqp3Vc4w4JDCYAoqTbunQ12" http://localhost:8000/api/users
- Get user by ID
$ curl -X GET --header "Content-Type: application/json" --header "Authorization: Bearer z7ZOw6IqUbDHLdozERIwp4w9Ti8wfvaI51b8xTqp3Vc4w4JDCYAoqTbunQ12" http://localhost:8000/api/user/1
- Get user profile
$ curl -X GET --header "Content-Type: application/json" --header "Authorization: Bearer z7ZOw6IqUbDHLdozERIwp4w9Ti8wfvaI51b8xTqp3Vc4w4JDCYAoqTbunQ12" http://localhost:8000/api/profile
- Add product
$ curl -X POST --header "Content-Type: application/json" --header "Authorization: Bearer z7ZOw6IqUbDHLdozERIwp4w9Ti8wfvaI51b8xTqp3Vc4w4JDCYAoqTbunQ12" --data '{"name":"Nike Air Force 1 Shadow", "quantity":300, "price":4500.00}' http://localhost:8000/api/product
- Get all products
$ curl -X GET --header "Content-Type: application/json" --header "Authorization: Bearer z7ZOw6IqUbDHLdozERIwp4w9Ti8wfvaI51b8xTqp3Vc4w4JDCYAoqTbunQ12" http://localhost:8000/api/products
- Get product by ID
$ curl -X GET --header "Content-Type: application/json" --header "Authorization: Bearer z7ZOw6IqUbDHLdozERIwp4w9Ti8wfvaI51b8xTqp3Vc4w4JDCYAoqTbunQ12" http://localhost:8000/api/product/1
HTTP/1.1 200 OK
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 29
{"id":1,"name":"Product One","quantity":23,"price":400.5,"created_at":"2024-03-14T15:59:26.000000Z","updated_at":"2024-03-14T15:59:26.000000Z"}
For the products endpoints we will be throttled at 30 requests per minute as per our API product limiter.
**_2. Web_**
On the web, all our authentication related pages will be rate limited at 10 requests per minute as per our auth limiter configuration while those pages concerning products will be throttled at 30 requests per minute.
- Throttled authentication requests
![Throttled auth requests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/92g91146zuno2kjjxv4r.png)
- Throttled product requests
![Throttled product requests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u0mroaipt3wys88acn2j.png)
- User credentials bruteforce attack prevented on login page
![User login page bruteforce blocked](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lyh22gf8ekrdwdvj15gx.png)
**Finally**
As we have seen, rate limiting in Laravel is a powerful tool for controlling the flow of requests to your application, ensuring its stability and security. By following the guidelines outlined in this article, you can effectively implement rate limiting to protect your application from abuse and ensure a smooth user experience.
**Let's connect**
- Connect on [LinkedIn](https://www.linkedin.com/in/benson-macharia)
- Check out my [GitHub](https://github.com/bensonmacharia)
Posted on March 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.