A Vapor middleware that effectively protects your application against brute-force attacks. Easy and fast to integrate. Optimized with Fluent for seamless Postgres support. Also provides login attempt tracking.
- Limits the number of login attempts per IP and email
- Gentle with legitimate users who make mistakes, relentless with attackers
- Exponential penalty increases after each set of consecutive failed attempts (customizable base time frame)
- Effectively protects against brute-force attacks, even when facing advanced techniques such as massive, concurrent (parallel) request attempts.
- Logs all suspicious activities and lockouts
- Easy integration into any existing Vapor project
This documentation assumes you already have an existing Vapor project.
- You have added Fluent as a dependency and are using a Postgres database.
If you do not have this setup, please follow the official Vapor getting started documentation.
You may also need a User model that conforms to the ModelAuthenticable protocol to enable authentication features.
If you need to implement this, refer to the official Vapor authentication documentation.
Add this package to your Package.swift dependencies:
.package(url: "https://github.com/Bilalyyy/VaporRateLimiter", from: "1.0.0")And add "VaporRateLimiter" to your target dependencies.
.product(name: "VaporRateLimiter", package: "VaporRateLimiter")To work properly, VaporRateLimiter requires a new model in your database to track login attempts.
This means you need to run a migration.
Make sure to add CreateConnexionAttempt() to your migration list.
If you want to use the sign-up rate limiter, add CreateSignUpAttempt() as well.
For example, in your configure.swift file:
import VaporRateLimiter
public func configure(_ app: Application) async throws {
//...
app.migrations.add(CreateConnexionAttempt())
app.migrations.add(CreateSignUpAttempt())
//...
}Apply the RateLimiter middleware to your login route to enable brute-force protection.
Apply the LoginRateLimiter middleware to your login route to enable brute-force protection.
For example:
import Vapor
import VaporRateLimiter
struct AuthController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
let routes = routes.grouped("api", "v1", "auth")
// ...
let limitedRoutes = routes.grouped(LoginRateLimiter())
limitedRoutes.post("login", use: loginHandler)
// ...
}
}By default, the threshold for failed attempts before applying the exponential penalty is set to five. This means a user can make up to five incorrect login attempts before being subject to a penalty (a waiting period during which further login attempts are blocked). You can customize this value to fit your security needs:
let limitedRoutes = routes.grouped(LoginRateLimiter(threshold: Int))You can also customize the base time frame (in seconds) for the exponential penalty:
let limitedRoutes = routes.grouped(LoginRateLimiter(baseTimeFrame: 120))VaporRateLimiter is a middleware that intercepts incoming requests on the routes where it is applied before they reach your route handlers.
When a request is intercepted, the middleware attempts to record an entry in the database to track attempts from the sender.
To do this, it uses a unique key to identify the sender—by default, this key is the user's email address (the value associated with the "mail" field in your request data).
Important:
If the middleware does not find a value for the expected key, it cannot register a new entry and the process will fail.
Make sure you are using the correct key for your use case.
If you want to track attempts using a different identifier (for example, an API key or username), you can customize the key as follows:
// Make sure you use the key used in your request
let limitedRoutes = routes.grouped(LoginRateLimiter(keyToRegister: "apiKey"))This flexibility allows you to adapt VaporRateLimiter to a variety of use cases—whether you’re protecting login endpoints, API access, or any other sensitive route.
Apply the SignUpRateLimiter middleware to your sign-up route. This limiter buckets IPs (/24 for IPv4, /64 for IPv6) to reduce noise from NAT pools and shared networks.
import Vapor
import VaporRateLimiter
struct AuthController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
let routes = routes.grouped("api", "v1", "auth")
// ...
let limitedRoutes = routes.grouped(SignUpRateLimiter())
limitedRoutes.post("signup", use: signupHandler)
// ...
}
}Defaults: threshold = 2, baseTimeFrame = 240 seconds. You can override them:
let limitedRoutes = routes.grouped(SignUpRateLimiter(threshold: 3, baseTimeFrame: 300))
⚠️ Note: For safety and convenience, the rate limiter middleware is disabled in the development environment.
⚠️ Note: Do not confuse the key intercepted from the request by VaporRateLimiter (for example,"mail"or"apiKey") with the database field where the value is stored.
The key you specify in the middleware (keyToRegister) can be changed to suit your needs,
but the field used in the database is always the same:key_id(in theConnexionAttemptmodel).
To prevent your users from being locked out for hours due to old, accumulated failed attempts, it’s important to clear their login attempts after a successful authentication.
This is usually done in your loginHandler function.
Make sure to add the following line after a successful login:
private func loginHandler(_ req: Request) async throws -> HTTPStatus {
// ... authentication logic ...
// ... After the user is authenticated
try await req.connexionAttempsSvc.userIsLoged(user.mail)
// ...
}If you use SignUpRateLimiter, clear the sign-up attempts once the account has been created successfully:
private func signupHandler(_ req: Request) async throws -> HTTPStatus {
// ... sign-up logic ...
// ... After the user is created
try await req.signUpAttempsSvc.userIsLoged(user.mail)
// ...
}For contribution guidelines, see CONTRIBUTING.
Feel free to open an issue or contact me by email. You can also find me on Linkedin.
MIT © Bilalyyy
