Packages

phoenix_kit

1.7.71
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit users rate_limiter.ex
Raw

lib/phoenix_kit/users/rate_limiter.ex

defmodule PhoenixKit.Users.RateLimiter.Backend do
@moduledoc """
Hammer 7.x backend for rate limiting.
"""
use Hammer, backend: :ets
end
defmodule PhoenixKit.Users.RateLimiter do
@moduledoc """
Rate limiting for authentication endpoints to prevent brute-force attacks.
This module provides comprehensive rate limiting protection for:
- Login attempts (prevents password brute-forcing)
- Magic link generation (prevents token enumeration)
- Password reset requests (prevents mass reset attacks)
- User registration (prevents spam account creation)
## Configuration
Rate limits can be configured in your application config:
# config/config.exs
config :phoenix_kit, PhoenixKit.Users.RateLimiter,
login_limit: 5, # Max login attempts per window
login_window_ms: 60_000, # 1 minute window
magic_link_limit: 3, # Max magic link requests per window
magic_link_window_ms: 300_000, # 5 minute window
password_reset_limit: 3, # Max password reset requests per window
password_reset_window_ms: 300_000, # 5 minute window
registration_limit: 3, # Max registration attempts per window
registration_window_ms: 3600_000, # 1 hour window
registration_ip_limit: 10, # Max registrations per IP per window
registration_ip_window_ms: 3600_000 # 1 hour window
## Security Features
- **Email-based rate limiting**: Prevents targeted attacks on specific accounts
- **IP-based rate limiting**: Prevents distributed attacks from single sources
- **Timing attack mitigation**: Consistent response times for valid/invalid emails
- **Exponential backoff**: Automatically enforced through time windows
- **Comprehensive logging**: All rate limit violations are logged for security monitoring
## Usage Examples
# Check login rate limit
case PhoenixKit.Users.RateLimiter.check_login_rate_limit(email, ip_address) do
:ok -> proceed_with_login()
{:error, :rate_limit_exceeded} -> show_rate_limit_error()
end
# Check magic link rate limit
case PhoenixKit.Users.RateLimiter.check_magic_link_rate_limit(email) do
:ok -> generate_magic_link()
{:error, :rate_limit_exceeded} -> show_cooldown_message()
end
## Production Recommendations
- Use Redis backend for distributed systems (hammer_backend_redis)
- Monitor rate limit violations for security threats
- Adjust limits based on your application's usage patterns
- Consider implementing CAPTCHA after multiple violations
"""
require Logger
alias PhoenixKit.Users.RateLimiter.Backend
@default_config [
# Login: 5 attempts per minute per email
login_limit: 5,
login_window_ms: 60_000,
# Magic link: 3 requests per 5 minutes per email
magic_link_limit: 3,
magic_link_window_ms: 300_000,
# Password reset: 3 requests per 5 minutes per email
password_reset_limit: 3,
password_reset_window_ms: 300_000,
# Registration: 3 attempts per hour per email
registration_limit: 3,
registration_window_ms: 3_600_000,
# Registration IP: 10 attempts per hour per IP
registration_ip_limit: 10,
registration_ip_window_ms: 3_600_000
]
@doc """
Checks if login attempts are within rate limit.
Returns `:ok` if the request is allowed, or `{:error, :rate_limit_exceeded}` if the limit is exceeded.
This function implements dual rate limiting:
- Per-email rate limiting (prevents targeted attacks on specific accounts)
- Per-IP rate limiting (prevents distributed brute-force attacks)
## Examples
iex> PhoenixKit.Users.RateLimiter.check_login_rate_limit("user@example.com", "192.168.1.1")
:ok
# After 5 failed attempts:
iex> PhoenixKit.Users.RateLimiter.check_login_rate_limit("user@example.com", "192.168.1.1")
{:error, :rate_limit_exceeded}
"""
def check_login_rate_limit(email, ip_address \\ nil) when is_binary(email) do
email = normalize_email(email)
config = get_config()
# Check email-based rate limit
email_key = "auth:login:email:#{email}"
limit = Keyword.get(config, :login_limit)
window = Keyword.get(config, :login_window_ms)
case check_rate_limit(email_key, window, limit) do
:ok ->
# Also check IP-based rate limit if IP is provided
if ip_address do
ip_key = "auth:login:ip:#{ip_address}"
# Allow slightly higher limit for IP (to avoid false positives in shared networks)
ip_limit = limit * 3
case check_rate_limit(ip_key, window, ip_limit) do
:ok ->
:ok
{:error, :rate_limit_exceeded} = error ->
log_rate_limit_violation("login", "ip:#{ip_address}", ip_limit, window)
error
end
else
:ok
end
{:error, :rate_limit_exceeded} = error ->
log_rate_limit_violation("login", "email:#{email}", limit, window)
error
end
end
@doc """
Checks if magic link generation is within rate limit.
Returns `:ok` if the request is allowed, or `{:error, :rate_limit_exceeded}` if the limit is exceeded.
Magic links have stricter rate limits to prevent token enumeration attacks.
## Examples
iex> PhoenixKit.Users.RateLimiter.check_magic_link_rate_limit("user@example.com")
:ok
# After 3 requests in 5 minutes:
iex> PhoenixKit.Users.RateLimiter.check_magic_link_rate_limit("user@example.com")
{:error, :rate_limit_exceeded}
"""
def check_magic_link_rate_limit(email) when is_binary(email) do
email = normalize_email(email)
config = get_config()
key = "auth:magic_link:#{email}"
limit = Keyword.get(config, :magic_link_limit)
window = Keyword.get(config, :magic_link_window_ms)
case check_rate_limit(key, window, limit) do
:ok ->
:ok
{:error, :rate_limit_exceeded} = error ->
log_rate_limit_violation("magic_link", email, limit, window)
error
end
end
@doc """
Checks if password reset requests are within rate limit.
Returns `:ok` if the request is allowed, or `{:error, :rate_limit_exceeded}` if the limit is exceeded.
Password reset requests have moderate rate limits to prevent mass reset attacks
while still allowing legitimate users to recover their accounts.
## Examples
iex> PhoenixKit.Users.RateLimiter.check_password_reset_rate_limit("user@example.com")
:ok
# After 3 requests in 5 minutes:
iex> PhoenixKit.Users.RateLimiter.check_password_reset_rate_limit("user@example.com")
{:error, :rate_limit_exceeded}
"""
def check_password_reset_rate_limit(email) when is_binary(email) do
email = normalize_email(email)
config = get_config()
key = "auth:password_reset:#{email}"
limit = Keyword.get(config, :password_reset_limit)
window = Keyword.get(config, :password_reset_window_ms)
case check_rate_limit(key, window, limit) do
:ok ->
:ok
{:error, :rate_limit_exceeded} = error ->
log_rate_limit_violation("password_reset", email, limit, window)
error
end
end
@doc """
Checks if registration attempts are within rate limit.
Returns `:ok` if the request is allowed, or `{:error, :rate_limit_exceeded}` if the limit is exceeded.
Registration has dual rate limiting:
- Per-email rate limiting (prevents spam account creation with same email)
- Per-IP rate limiting (prevents mass account creation from single source)
## Examples
iex> PhoenixKit.Users.RateLimiter.check_registration_rate_limit("user@example.com", "192.168.1.1")
:ok
# After limit exceeded:
iex> PhoenixKit.Users.RateLimiter.check_registration_rate_limit("user@example.com", "192.168.1.1")
{:error, :rate_limit_exceeded}
"""
def check_registration_rate_limit(email, ip_address \\ nil) when is_binary(email) do
email = normalize_email(email)
config = get_config()
# Check email-based rate limit
email_key = "auth:registration:email:#{email}"
email_limit = Keyword.get(config, :registration_limit)
email_window = Keyword.get(config, :registration_window_ms)
case check_rate_limit(email_key, email_window, email_limit) do
:ok ->
# Also check IP-based rate limit if IP is provided
if ip_address do
ip_key = "auth:registration:ip:#{ip_address}"
ip_limit = Keyword.get(config, :registration_ip_limit)
ip_window = Keyword.get(config, :registration_ip_window_ms)
case check_rate_limit(ip_key, ip_window, ip_limit) do
:ok ->
:ok
{:error, :rate_limit_exceeded} = error ->
log_rate_limit_violation("registration", "ip:#{ip_address}", ip_limit, ip_window)
error
end
else
:ok
end
{:error, :rate_limit_exceeded} = error ->
log_rate_limit_violation("registration", "email:#{email}", email_limit, email_window)
error
end
end
@doc """
Resets rate limit for a specific action and identifier.
**DEPRECATED:** Hammer 7.x removed `delete_buckets` with no replacement.
This function now returns an error as Backend.set/3 requires positive integers (cannot set to 0).
Rate limits will naturally expire after their configured window period.
## Migration
- **For testing**: Use `Application.put_env` to disable rate limiting
- **For admin intervention**: Wait for the time window to expire
- **For immediate reset**: Restart the application (clears ETS tables)
See: https://hexdocs.pm/hammer/upgrade-v7.html
## Examples
iex> PhoenixKit.Users.RateLimiter.reset_rate_limit(:login, "email:user@example.com")
{:error, :not_supported}
"""
@deprecated "Hammer 7.x removed delete_buckets. Rate limits expire after their time window."
def reset_rate_limit(_action, _identifier) do
Logger.warning(
"PhoenixKit.RateLimiter.reset_rate_limit/2 is deprecated. " <>
"Rate limits expire automatically after their configured time window."
)
{:error, :not_supported}
end
@doc """
Gets the remaining attempts for a specific action and identifier.
Returns the number of attempts remaining before rate limit is exceeded.
For login and registration actions, returns the email-based limit.
For magic_link and password_reset, returns the limit for the email.
Note: With Hammer 7.x, this uses the get/2 function to retrieve the current count.
## Examples
iex> PhoenixKit.Users.RateLimiter.get_remaining_attempts(:login, "user@example.com")
5
iex> PhoenixKit.Users.RateLimiter.get_remaining_attempts(:magic_link, "user@example.com")
3
"""
def get_remaining_attempts(action, identifier) when is_atom(action) and is_binary(identifier) do
identifier =
if action in [:magic_link, :password_reset] do
normalize_email(identifier)
else
# For login and registration, assume email identifier and add prefix
"email:#{normalize_email(identifier)}"
end
config = get_config()
key = "auth:#{action}:#{identifier}"
{limit, window} =
case action do
:login ->
{Keyword.get(config, :login_limit), Keyword.get(config, :login_window_ms)}
:magic_link ->
{Keyword.get(config, :magic_link_limit), Keyword.get(config, :magic_link_window_ms)}
:password_reset ->
{Keyword.get(config, :password_reset_limit),
Keyword.get(config, :password_reset_window_ms)}
:registration ->
{Keyword.get(config, :registration_limit), Keyword.get(config, :registration_window_ms)}
end
# Hammer 7.x: Use get/2 to retrieve the current count
# Backend.get/2 returns an integer directly (current count)
count = Backend.get(key, window)
max(0, limit - count)
end
# Private functions
defp check_rate_limit(key, window_ms, limit) do
# Hammer 7.x: Backend.hit/3 returns {:allow, count} or {:deny, retry_after}
case Backend.hit(key, window_ms, limit) do
{:allow, _count} ->
:ok
{:deny, _retry_after_ms} ->
{:error, :rate_limit_exceeded}
end
end
defp normalize_email(email) do
email
|> String.trim()
|> String.downcase()
end
def get_config do
PhoenixKit.Config.get(__MODULE__, [])
|> Keyword.merge(@default_config, fn _k, v1, _v2 -> v1 end)
end
defp log_rate_limit_violation(action, identifier, limit, window_ms) do
window_description = format_window(window_ms)
Logger.warning(
"PhoenixKit.RateLimiter: Rate limit exceeded for #{action} - " <>
"#{identifier} exceeded #{limit} attempts in #{window_description}"
)
end
defp format_window(ms) when ms < 60_000, do: "#{div(ms, 1000)} seconds"
defp format_window(ms) when ms < 3_600_000, do: "#{div(ms, 60_000)} minutes"
defp format_window(ms), do: "#{div(ms, 3_600_000)} hours"
end