asap-protocol 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,359 @@
1
+ """Authentication middleware for ASAP protocol server.
2
+
3
+ This module provides authentication middleware that:
4
+ - Validates Bearer tokens based on manifest configuration
5
+ - Verifies sender identity matches authenticated agent
6
+ - Supports custom token validation logic
7
+ - Returns proper JSON-RPC error responses for auth failures
8
+
9
+ Example:
10
+ >>> from asap.transport.middleware import AuthenticationMiddleware, BearerTokenValidator
11
+ >>> from asap.models.entities import Manifest, AuthScheme
12
+ >>>
13
+ >>> # Create manifest with auth configuration
14
+ >>> manifest = Manifest(
15
+ ... id="urn:asap:agent:secure-agent",
16
+ ... name="Secure Agent",
17
+ ... version="1.0.0",
18
+ ... description="Agent with authentication",
19
+ ... auth=AuthScheme(schemes=["bearer"]),
20
+ ... # ... other fields
21
+ >>> )
22
+ >>>
23
+ >>> # Create custom token validator
24
+ >>> def validate_token(token: str) -> str | None:
25
+ ... # Validate token and return agent_id if valid
26
+ ... if token == "valid-token-123":
27
+ ... return "urn:asap:agent:authorized-client"
28
+ ... return None
29
+ >>>
30
+ >>> validator = BearerTokenValidator(validate_token)
31
+ >>> middleware = AuthenticationMiddleware(manifest, validator)
32
+ """
33
+
34
+ import hashlib
35
+ from typing import Callable, Protocol
36
+
37
+ from fastapi import HTTPException, Request
38
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
39
+
40
+ from asap.models.entities import Manifest
41
+ from asap.observability import get_logger
42
+
43
+ logger = get_logger(__name__)
44
+
45
+ # Authentication header scheme
46
+ AUTH_SCHEME_BEARER = "bearer"
47
+
48
+ # HTTP status codes
49
+ HTTP_UNAUTHORIZED = 401
50
+ HTTP_FORBIDDEN = 403
51
+
52
+ # Error messages
53
+ ERROR_AUTH_REQUIRED = "Authentication required"
54
+ ERROR_INVALID_TOKEN = "Invalid authentication token"
55
+ ERROR_SENDER_MISMATCH = "Sender does not match authenticated identity"
56
+
57
+
58
+ class TokenValidator(Protocol):
59
+ """Protocol for token validation implementations.
60
+
61
+ Custom validators must implement this interface to integrate
62
+ with the authentication middleware.
63
+
64
+ Example:
65
+ >>> class MyTokenValidator:
66
+ ... def __call__(self, token: str) -> str | None:
67
+ ... # Validate token against database, JWT, etc.
68
+ ... if is_valid(token):
69
+ ... return extract_agent_id(token)
70
+ ... return None
71
+ """
72
+
73
+ def __call__(self, token: str) -> str | None:
74
+ """Validate a token and return the authenticated agent ID.
75
+
76
+ Args:
77
+ token: The authentication token to validate
78
+
79
+ Returns:
80
+ The agent ID (URN) if token is valid, None otherwise
81
+
82
+ Example:
83
+ >>> validator = BearerTokenValidator(my_validate_func)
84
+ >>> agent_id = validator("token-123")
85
+ >>> print(agent_id) # "urn:asap:agent:client-1" or None
86
+ """
87
+ ...
88
+
89
+
90
+ class BearerTokenValidator:
91
+ """Default Bearer token validator implementation.
92
+
93
+ Wraps a validation function to conform to the TokenValidator protocol.
94
+ The validation function should take a token string and return an agent ID
95
+ if valid, or None if invalid.
96
+
97
+ Attributes:
98
+ validate_func: Function that validates tokens and returns agent IDs
99
+
100
+ Example:
101
+ >>> def my_validator(token: str) -> str | None:
102
+ ... # Check token in database
103
+ ... if token in valid_tokens:
104
+ ... return valid_tokens[token]["agent_id"]
105
+ ... return None
106
+ >>>
107
+ >>> validator = BearerTokenValidator(my_validator)
108
+ >>> agent_id = validator("abc123")
109
+ """
110
+
111
+ def __init__(self, validate_func: Callable[[str], str | None]) -> None:
112
+ """Initialize the Bearer token validator.
113
+
114
+ Args:
115
+ validate_func: Function that validates tokens and returns agent IDs
116
+ """
117
+ self.validate_func = validate_func
118
+
119
+ def __call__(self, token: str) -> str | None:
120
+ """Validate a token and return the authenticated agent ID.
121
+
122
+ Args:
123
+ token: The authentication token to validate
124
+
125
+ Returns:
126
+ The agent ID (URN) if token is valid, None otherwise
127
+ """
128
+ return self.validate_func(token)
129
+
130
+
131
+ class AuthenticationMiddleware:
132
+ """FastAPI middleware for ASAP protocol authentication.
133
+
134
+ This middleware handles authentication based on the manifest configuration:
135
+ - If manifest has no auth config, authentication is skipped
136
+ - If auth is configured, validates Bearer tokens
137
+ - Verifies sender in envelope matches authenticated identity
138
+ - Returns proper JSON-RPC error responses for failures
139
+
140
+ Attributes:
141
+ manifest: The agent manifest with auth configuration
142
+ validator: Token validator implementation
143
+ security: HTTPBearer security scheme from FastAPI
144
+
145
+ Example:
146
+ >>> from asap.transport.middleware import AuthenticationMiddleware
147
+ >>> from asap.models.entities import Manifest, AuthScheme
148
+ >>>
149
+ >>> manifest = Manifest(
150
+ ... id="urn:asap:agent:my-agent",
151
+ ... auth=AuthScheme(schemes=["bearer"]),
152
+ ... # ... other fields
153
+ ... )
154
+ >>>
155
+ >>> def validate_token(token: str) -> str | None:
156
+ ... # Your validation logic
157
+ ... return "urn:asap:agent:client" if valid else None
158
+ >>>
159
+ >>> validator = BearerTokenValidator(validate_token)
160
+ >>> middleware = AuthenticationMiddleware(manifest, validator)
161
+ """
162
+
163
+ def __init__(
164
+ self,
165
+ manifest: Manifest,
166
+ validator: TokenValidator | None = None,
167
+ ) -> None:
168
+ """Initialize authentication middleware.
169
+
170
+ Args:
171
+ manifest: Agent manifest with auth configuration
172
+ validator: Token validator implementation (optional if auth not required)
173
+
174
+ Raises:
175
+ ValueError: If manifest requires auth but no validator provided
176
+ """
177
+ self.manifest = manifest
178
+ self.validator = validator
179
+ self.security = HTTPBearer(auto_error=False)
180
+
181
+ # Validate configuration
182
+ if self._is_auth_required() and validator is None:
183
+ raise ValueError(
184
+ "Token validator required when authentication is configured in manifest"
185
+ )
186
+
187
+ def _is_auth_required(self) -> bool:
188
+ """Check if authentication is required by manifest.
189
+
190
+ Returns:
191
+ True if manifest has auth configuration, False otherwise
192
+ """
193
+ return self.manifest.auth is not None
194
+
195
+ def _supports_bearer_auth(self) -> bool:
196
+ """Check if manifest supports Bearer token authentication.
197
+
198
+ Returns:
199
+ True if "bearer" is in auth schemes, False otherwise
200
+ """
201
+ if not self.manifest.auth:
202
+ return False
203
+ return AUTH_SCHEME_BEARER in self.manifest.auth.schemes
204
+
205
+ async def verify_authentication(
206
+ self,
207
+ request: Request,
208
+ credentials: HTTPAuthorizationCredentials | None = None,
209
+ ) -> str | None:
210
+ """Verify authentication for an incoming request.
211
+
212
+ This method:
213
+ 1. Checks if authentication is required
214
+ 2. Extracts credentials from Authorization header
215
+ 3. Validates token using the configured validator
216
+ 4. Returns authenticated agent ID or raises HTTPException
217
+
218
+ Args:
219
+ request: The incoming FastAPI request
220
+ credentials: Optional pre-extracted credentials
221
+
222
+ Returns:
223
+ The authenticated agent ID (URN) if auth is required and valid,
224
+ None if auth is not required
225
+
226
+ Raises:
227
+ HTTPException: If authentication is required but fails (401 or 403)
228
+
229
+ Example:
230
+ >>> middleware = AuthenticationMiddleware(manifest, validator)
231
+ >>> agent_id = await middleware.verify_authentication(request)
232
+ >>> # agent_id is None if auth not required
233
+ >>> # agent_id is agent URN if auth successful
234
+ >>> # HTTPException raised if auth failed
235
+ """
236
+ # Skip authentication if not required
237
+ if not self._is_auth_required():
238
+ logger.debug("asap.auth.skipped", reason="not_required_by_manifest")
239
+ return None
240
+
241
+ # Extract credentials if not provided
242
+ if credentials is None:
243
+ credentials = await self.security(request)
244
+
245
+ # Require credentials if auth is configured
246
+ if credentials is None:
247
+ logger.warning("asap.auth.missing", manifest_id=self.manifest.id)
248
+ raise HTTPException(
249
+ status_code=HTTP_UNAUTHORIZED,
250
+ detail=ERROR_AUTH_REQUIRED,
251
+ headers={"WWW-Authenticate": "Bearer"},
252
+ )
253
+
254
+ # Validate Authorization scheme (case-insensitive)
255
+ if credentials.scheme.lower() != AUTH_SCHEME_BEARER:
256
+ logger.warning(
257
+ "asap.auth.invalid_scheme",
258
+ manifest_id=self.manifest.id,
259
+ provided_scheme=credentials.scheme,
260
+ expected_scheme=AUTH_SCHEME_BEARER,
261
+ )
262
+ raise HTTPException(
263
+ status_code=HTTP_UNAUTHORIZED,
264
+ detail=ERROR_INVALID_TOKEN,
265
+ headers={"WWW-Authenticate": "Bearer"},
266
+ )
267
+
268
+ # Validate Bearer token support in manifest
269
+ if not self._supports_bearer_auth():
270
+ logger.warning(
271
+ "asap.auth.scheme_not_supported",
272
+ manifest_id=self.manifest.id,
273
+ provided_scheme=credentials.scheme,
274
+ )
275
+ raise HTTPException(
276
+ status_code=HTTP_UNAUTHORIZED,
277
+ detail=ERROR_INVALID_TOKEN,
278
+ headers={"WWW-Authenticate": "Bearer"},
279
+ )
280
+
281
+ # Validate token and get agent ID
282
+ token = credentials.credentials
283
+ # Type narrowing: validator is not None when auth is required (validated in __init__)
284
+ if self.validator is None:
285
+ raise RuntimeError(
286
+ "Token validator is None but authentication is required. "
287
+ "This should not happen if middleware was initialized correctly."
288
+ )
289
+ agent_id = self.validator(token)
290
+
291
+ if agent_id is None:
292
+ # Log token hash instead of prefix to avoid exposing token data
293
+ token_hash = hashlib.sha256(token.encode()).hexdigest()[:16]
294
+ logger.warning(
295
+ "asap.auth.invalid_token",
296
+ manifest_id=self.manifest.id,
297
+ token_hash=token_hash,
298
+ )
299
+ raise HTTPException(
300
+ status_code=HTTP_UNAUTHORIZED,
301
+ detail=ERROR_INVALID_TOKEN,
302
+ headers={"WWW-Authenticate": "Bearer"},
303
+ )
304
+
305
+ logger.info(
306
+ "asap.auth.success",
307
+ manifest_id=self.manifest.id,
308
+ authenticated_agent=agent_id,
309
+ )
310
+ return agent_id
311
+
312
+ def verify_sender_matches_auth(
313
+ self,
314
+ authenticated_agent_id: str | None,
315
+ envelope_sender: str,
316
+ ) -> None:
317
+ """Verify that envelope sender matches authenticated identity.
318
+
319
+ This prevents agents from spoofing the sender field in the envelope.
320
+ Only enforced when authentication is enabled.
321
+
322
+ Args:
323
+ authenticated_agent_id: The agent ID from authentication, or None
324
+ envelope_sender: The sender field from the envelope
325
+
326
+ Raises:
327
+ HTTPException: If sender doesn't match authenticated identity (403)
328
+
329
+ Example:
330
+ >>> middleware.verify_sender_matches_auth(
331
+ ... authenticated_agent_id="urn:asap:agent:client-1",
332
+ ... envelope_sender="urn:asap:agent:client-1"
333
+ ... ) # OK
334
+ >>>
335
+ >>> middleware.verify_sender_matches_auth(
336
+ ... authenticated_agent_id="urn:asap:agent:client-1",
337
+ ... envelope_sender="urn:asap:agent:spoofed"
338
+ ... ) # Raises HTTPException
339
+ """
340
+ # Skip verification if auth is not enabled
341
+ if authenticated_agent_id is None:
342
+ return
343
+
344
+ # Verify sender matches authenticated identity
345
+ if envelope_sender != authenticated_agent_id:
346
+ logger.warning(
347
+ "asap.auth.sender_mismatch",
348
+ authenticated_agent=authenticated_agent_id,
349
+ envelope_sender=envelope_sender,
350
+ )
351
+ raise HTTPException(
352
+ status_code=HTTP_FORBIDDEN,
353
+ detail=ERROR_SENDER_MISMATCH,
354
+ )
355
+
356
+ logger.debug(
357
+ "asap.auth.sender_verified",
358
+ authenticated_agent=authenticated_agent_id,
359
+ )