|
1 | 1 | import inspect |
| 2 | +import logging |
2 | 3 | from abc import ABC, abstractmethod |
3 | 4 | from functools import lru_cache |
| 5 | +from logging import Logger |
4 | 6 | from typing import Any, List, Optional, Sequence, Type, Union |
5 | 7 |
|
6 | 8 | from rodi import ContainerProtocol |
7 | 9 |
|
8 | 10 | from guardpost.abc import BaseStrategy |
| 11 | +from guardpost.protection import InvalidCredentialsError, RateLimiter |
9 | 12 |
|
10 | 13 |
|
11 | 14 | class Identity: |
@@ -108,9 +111,26 @@ def __init__( |
108 | 111 | self, |
109 | 112 | *handlers: AuthenticationHandlerConfType, |
110 | 113 | container: Optional[ContainerProtocol] = None, |
| 114 | + rate_limiter: Optional[RateLimiter] = None, |
| 115 | + logger: Optional[Logger] = None, |
111 | 116 | ): |
| 117 | + """ |
| 118 | + Initializes an AuthenticationStrategy instance. |
| 119 | +
|
| 120 | + Args: |
| 121 | + *handlers: One or more authentication handler instances or types to be used |
| 122 | + for authentication. |
| 123 | + container: Optional dependency injection container for resolving handler |
| 124 | + instances. |
| 125 | + rate_limiter: Optional RateLimiter to apply rate limiting to authentication |
| 126 | + attempts. |
| 127 | + logger: Optional logger instance for logging authentication events. If not |
| 128 | + provided, defaults to `logging.getLogger("guardpost")` |
| 129 | + """ |
112 | 130 | super().__init__(container) |
113 | 131 | self.handlers = list(handlers) |
| 132 | + self._logger = logger or logging.getLogger("guardpost") |
| 133 | + self._rate_limiter = rate_limiter |
114 | 134 |
|
115 | 135 | def add(self, handler: AuthenticationHandlerConfType) -> "AuthenticationStrategy": |
116 | 136 | self.handlers.append(handler) |
@@ -151,21 +171,49 @@ async def authenticate( |
151 | 171 | self, context: Any, authentication_schemes: Optional[Sequence[str]] = None |
152 | 172 | ) -> Optional[Identity]: |
153 | 173 | """ |
154 | | - Tries to obtain the user for a context, applying authentication rules. |
| 174 | + Tries to obtain the user for a context, applying authentication rules and |
| 175 | + optional rate limiting. |
155 | 176 | """ |
156 | 177 | if not context: |
157 | 178 | raise ValueError("Missing context to evaluate authentication") |
158 | 179 |
|
| 180 | + if self._rate_limiter: |
| 181 | + await self._rate_limiter.validate_authentication_attempt(context) |
| 182 | + |
| 183 | + identity = None |
159 | 184 | for handler in self._get_handlers_by_schemes(authentication_schemes, context): |
160 | | - if _is_async_handler(type(handler)): |
161 | | - identity = await handler.authenticate(context) # type: ignore |
162 | | - else: |
163 | | - identity = handler.authenticate(context) |
| 185 | + try: |
| 186 | + identity = await self._authenticate_with_handler(handler, context) |
| 187 | + except InvalidCredentialsError as invalid_credentials_error: |
| 188 | + # A client provided credentials of a given type, and they were invalid. |
| 189 | + # Store the information, so later calls can be validated without |
| 190 | + # attempting authentication. |
| 191 | + self._logger.info( |
| 192 | + "Invalid credentials received from client IP %s for scheme: %s", |
| 193 | + invalid_credentials_error.client_ip, |
| 194 | + handler.scheme, |
| 195 | + ) |
| 196 | + if self._rate_limiter: |
| 197 | + await self._rate_limiter.store_authentication_failure( |
| 198 | + invalid_credentials_error |
| 199 | + ) |
164 | 200 |
|
165 | 201 | if identity: |
166 | 202 | try: |
167 | 203 | context.identity = identity |
168 | 204 | except AttributeError: |
169 | 205 | pass |
170 | 206 | return identity |
| 207 | + else: |
| 208 | + try: |
| 209 | + if context.identity is None: |
| 210 | + context.identity = Identity() |
| 211 | + except AttributeError: |
| 212 | + pass |
171 | 213 | return None |
| 214 | + |
| 215 | + async def _authenticate_with_handler(self, handler: AuthenticationHandler, context): |
| 216 | + if _is_async_handler(type(handler)): |
| 217 | + return await handler.authenticate(context) # type: ignore |
| 218 | + else: |
| 219 | + return handler.authenticate(context) |
0 commit comments