22This module provides classes to protect against brute-force attacks.
33"""
44
5+ import logging
56from abc import ABC , abstractmethod
67from datetime import UTC , datetime
8+ from logging import Logger
79from typing import Any , Callable , Optional , Sequence
810
9- from guardpost .errors import InvalidCredentialsError
11+ from guardpost .errors import InvalidCredentialsError , RateLimitExceededError
1012
1113
1214class FailedAuthenticationAttempts :
@@ -140,6 +142,7 @@ def __init__(
140142 block_time : int = 60 ,
141143 store : Optional [AuthenticationAttemptsStore ] = None ,
142144 trusted_keys : Optional [Sequence [str ]] = None ,
145+ logger : Optional [Logger ] = None ,
143146 ) -> None :
144147 """
145148 Initialize a RateLimiter instance for brute-force protection.
@@ -171,25 +174,50 @@ def __init__(
171174 max_entry_age = self ._block_time + 5
172175 )
173176 self ._key_getter = key_getter
177+ self ._logger = logger or logging .getLogger ("guardpost" )
174178
175179 def get_context_key (self , context : Any ) -> str :
176180 """
177- Extracts the rate limiting key from the authentication context.
178-
179- Raises an AuthException if no key_getter is provided. Custom extractors
180- should be used carefully as they may introduce security vulnerabilities.
181+ Obtains the rate limiting key from the authentication context.
182+ If a key_getter is not configured, rate limiting is disabled and
183+ returns an empty string.
181184 """
182185 if not self ._key_getter :
183186 return ""
184187 return self ._key_getter (context )
185188
186- async def allow_authentication_attempt (self , context : Any ) -> bool :
189+ async def validate_authentication_attempt (self , context : Any ) -> None :
190+ """
191+ Checks if an authentication attempt should be allowed for the given context.
192+ It always returns true if a `key_getter` function is not configured (function to
193+ obtain a key from the user-defined authentication context).
194+
195+ Args:
196+ context: The authentication context (e.g., request object).
197+
198+ Raises:
199+ **RateLimitExceededError**: If the rate limit has been exceeded and the
200+ attempt is blocked.
201+ """
202+ key = self .get_context_key (context )
203+
204+ valid_context = await self .allow_authentication_attempt (key )
205+
206+ if not valid_context :
207+ self ._logger .warning (
208+ "Blocking authentication attempt for '%s' because of rate limiting." ,
209+ key ,
210+ )
211+ raise RateLimitExceededError ()
212+
213+ async def allow_authentication_attempt (self , key : Any ) -> bool :
187214 """
188215 Determines if an authentication attempt should be allowed based on rate limiting
189216 rules. Returns True if the attempt should proceed, False if it should be
190- blocked.
217+ blocked. Key can be a str or a context.
191218 """
192- key = self .get_context_key (context )
219+ if not isinstance (key , str ):
220+ key = self .get_context_key (key )
193221
194222 if not key :
195223 # BF protection disabled for backward compatibility.
@@ -212,7 +240,9 @@ async def allow_authentication_attempt(self, context: Any) -> bool:
212240
213241 return True
214242
215- async def store_authentication_failure (self , error : InvalidCredentialsError ):
243+ async def store_authentication_failure (
244+ self , error : InvalidCredentialsError
245+ ) -> FailedAuthenticationAttempts :
216246 """
217247 Tracks information about a failed authentication attempt.
218248 """
@@ -225,6 +255,7 @@ async def store_authentication_failure(self, error: InvalidCredentialsError):
225255 else :
226256 failed_attempt .increase_counter ()
227257 await self ._store .set_failed_attempts (failed_attempt )
258+ return failed_attempt
228259
229260
230261class SelfCleaningInMemoryAuthenticationAttemptsStore (
0 commit comments