mcp-security-framework 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.
Files changed (76) hide show
  1. mcp_security_framework/__init__.py +96 -0
  2. mcp_security_framework/cli/__init__.py +18 -0
  3. mcp_security_framework/cli/cert_cli.py +511 -0
  4. mcp_security_framework/cli/security_cli.py +791 -0
  5. mcp_security_framework/constants.py +209 -0
  6. mcp_security_framework/core/__init__.py +61 -0
  7. mcp_security_framework/core/auth_manager.py +1011 -0
  8. mcp_security_framework/core/cert_manager.py +1663 -0
  9. mcp_security_framework/core/permission_manager.py +735 -0
  10. mcp_security_framework/core/rate_limiter.py +602 -0
  11. mcp_security_framework/core/security_manager.py +943 -0
  12. mcp_security_framework/core/ssl_manager.py +735 -0
  13. mcp_security_framework/examples/__init__.py +75 -0
  14. mcp_security_framework/examples/django_example.py +615 -0
  15. mcp_security_framework/examples/fastapi_example.py +472 -0
  16. mcp_security_framework/examples/flask_example.py +506 -0
  17. mcp_security_framework/examples/gateway_example.py +803 -0
  18. mcp_security_framework/examples/microservice_example.py +690 -0
  19. mcp_security_framework/examples/standalone_example.py +576 -0
  20. mcp_security_framework/middleware/__init__.py +250 -0
  21. mcp_security_framework/middleware/auth_middleware.py +292 -0
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
  23. mcp_security_framework/middleware/fastapi_middleware.py +757 -0
  24. mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
  25. mcp_security_framework/middleware/flask_middleware.py +591 -0
  26. mcp_security_framework/middleware/mtls_middleware.py +439 -0
  27. mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
  28. mcp_security_framework/middleware/security_middleware.py +507 -0
  29. mcp_security_framework/schemas/__init__.py +109 -0
  30. mcp_security_framework/schemas/config.py +694 -0
  31. mcp_security_framework/schemas/models.py +709 -0
  32. mcp_security_framework/schemas/responses.py +686 -0
  33. mcp_security_framework/tests/__init__.py +0 -0
  34. mcp_security_framework/utils/__init__.py +121 -0
  35. mcp_security_framework/utils/cert_utils.py +525 -0
  36. mcp_security_framework/utils/crypto_utils.py +475 -0
  37. mcp_security_framework/utils/validation_utils.py +571 -0
  38. mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
  39. mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
  40. mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
  41. mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
  42. mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
  43. tests/__init__.py +0 -0
  44. tests/test_cli/__init__.py +0 -0
  45. tests/test_cli/test_cert_cli.py +379 -0
  46. tests/test_cli/test_security_cli.py +657 -0
  47. tests/test_core/__init__.py +0 -0
  48. tests/test_core/test_auth_manager.py +582 -0
  49. tests/test_core/test_cert_manager.py +795 -0
  50. tests/test_core/test_permission_manager.py +395 -0
  51. tests/test_core/test_rate_limiter.py +626 -0
  52. tests/test_core/test_security_manager.py +841 -0
  53. tests/test_core/test_ssl_manager.py +532 -0
  54. tests/test_examples/__init__.py +8 -0
  55. tests/test_examples/test_fastapi_example.py +264 -0
  56. tests/test_examples/test_flask_example.py +238 -0
  57. tests/test_examples/test_standalone_example.py +292 -0
  58. tests/test_integration/__init__.py +0 -0
  59. tests/test_integration/test_auth_flow.py +502 -0
  60. tests/test_integration/test_certificate_flow.py +527 -0
  61. tests/test_integration/test_fastapi_integration.py +341 -0
  62. tests/test_integration/test_flask_integration.py +398 -0
  63. tests/test_integration/test_standalone_integration.py +493 -0
  64. tests/test_middleware/__init__.py +0 -0
  65. tests/test_middleware/test_fastapi_middleware.py +523 -0
  66. tests/test_middleware/test_flask_middleware.py +582 -0
  67. tests/test_middleware/test_security_middleware.py +493 -0
  68. tests/test_schemas/__init__.py +0 -0
  69. tests/test_schemas/test_config.py +811 -0
  70. tests/test_schemas/test_models.py +879 -0
  71. tests/test_schemas/test_responses.py +1054 -0
  72. tests/test_schemas/test_serialization.py +493 -0
  73. tests/test_utils/__init__.py +0 -0
  74. tests/test_utils/test_cert_utils.py +510 -0
  75. tests/test_utils/test_crypto_utils.py +603 -0
  76. tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,591 @@
1
+ """
2
+ Flask Security Middleware Module
3
+
4
+ This module provides Flask-specific security middleware implementation
5
+ that integrates with Flask's WSGI system and request/response handling.
6
+
7
+ Key Features:
8
+ - Flask-specific request/response processing
9
+ - Integration with Flask WSGI system
10
+ - Flask-specific authentication methods
11
+ - Flask-specific error responses
12
+ - Flask-specific header management
13
+ - Flask-specific rate limiting
14
+
15
+ Classes:
16
+ FlaskSecurityMiddleware: Flask-specific security middleware
17
+ FlaskMiddlewareError: Flask middleware-specific error exception
18
+
19
+ Author: MCP Security Team
20
+ Version: 1.0.0
21
+ License: MIT
22
+ """
23
+
24
+ import json
25
+ import logging
26
+ from typing import Any, Dict, List, Optional, Union
27
+
28
+ from flask import Request, Response, request, make_response, jsonify, current_app
29
+
30
+ from .security_middleware import SecurityMiddleware, SecurityMiddlewareError
31
+ from ..schemas.models import AuthResult, AuthStatus, AuthMethod
32
+
33
+
34
+ class FlaskMiddlewareError(SecurityMiddlewareError):
35
+ """Raised when Flask middleware encounters an error."""
36
+
37
+ def __init__(self, message: str, error_code: int = -32020):
38
+ self.message = message
39
+ self.error_code = error_code
40
+ super().__init__(self.message)
41
+
42
+
43
+ class FlaskSecurityMiddleware(SecurityMiddleware):
44
+ """
45
+ Flask Security Middleware Class
46
+
47
+ This class provides Flask-specific implementation of the security
48
+ middleware. It integrates with Flask's WSGI system and handles
49
+ Flask Request/Response objects.
50
+
51
+ The FlaskSecurityMiddleware implements:
52
+ - Flask-specific request processing
53
+ - Flask authentication method handling
54
+ - Flask response creation and modification
55
+ - Flask-specific error handling
56
+ - Flask header management
57
+ - Flask rate limiting integration
58
+
59
+ Key Responsibilities:
60
+ - Process Flask requests through security pipeline
61
+ - Extract authentication credentials from Flask requests
62
+ - Create Flask-specific error responses
63
+ - Add security headers to Flask responses
64
+ - Handle Flask-specific request/response objects
65
+ - Integrate with Flask WSGI system
66
+
67
+ Attributes:
68
+ Inherits all attributes from SecurityMiddleware
69
+ _flask_app: Reference to Flask application (if available)
70
+
71
+ Example:
72
+ >>> from flask import Flask
73
+ >>> from mcp_security_framework.middleware import FlaskSecurityMiddleware
74
+ >>>
75
+ >>> app = Flask(__name__)
76
+ >>> security_manager = SecurityManager(config)
77
+ >>> middleware = FlaskSecurityMiddleware(security_manager)
78
+ >>> app.wsgi_app = middleware(app.wsgi_app)
79
+
80
+ Note:
81
+ This middleware should be integrated with Flask applications
82
+ by wrapping the WSGI application.
83
+ """
84
+
85
+ def __init__(self, security_manager):
86
+ """
87
+ Initialize Flask Security Middleware.
88
+
89
+ Args:
90
+ security_manager: Security manager instance containing
91
+ all security components and configuration.
92
+
93
+ Raises:
94
+ FlaskMiddlewareError: If initialization fails
95
+ """
96
+ super().__init__(security_manager)
97
+ self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
98
+
99
+ self.logger.info("Flask Security Middleware initialized")
100
+
101
+ def __call__(self, environ: Dict[str, Any], start_response) -> List[bytes]:
102
+ """
103
+ Process Flask request through security middleware.
104
+
105
+ This method implements the security processing pipeline for
106
+ Flask requests, including rate limiting, authentication,
107
+ authorization, and security header management.
108
+
109
+ Args:
110
+ environ (Dict[str, Any]): WSGI environment dictionary
111
+ start_response: WSGI start_response callable
112
+
113
+ Returns:
114
+ List[bytes]: WSGI response body
115
+
116
+ Raises:
117
+ FlaskMiddlewareError: For middleware processing errors
118
+ """
119
+ try:
120
+ # Create Flask request object from WSGI environ
121
+ flask_request = Request(environ)
122
+
123
+ # Check rate limit
124
+ if not self._check_rate_limit(flask_request):
125
+ return self._rate_limit_response(start_response)
126
+
127
+ # Check if public path
128
+ if self._is_public_path(flask_request):
129
+ # Process request normally
130
+ return self._process_request(environ, start_response, flask_request)
131
+
132
+ # Authenticate request
133
+ auth_result = self._authenticate_request(flask_request)
134
+ if not auth_result.is_valid:
135
+ return self._auth_error_response(auth_result, start_response)
136
+
137
+ # Validate permissions
138
+ if not self._validate_permissions(flask_request, auth_result):
139
+ return self._permission_error_response(start_response)
140
+
141
+ # Process request
142
+ return self._process_request(environ, start_response, flask_request, auth_result)
143
+
144
+ except Exception as e:
145
+ self.logger.error(
146
+ "Flask middleware processing failed",
147
+ extra={"error": str(e)},
148
+ exc_info=True
149
+ )
150
+ raise FlaskMiddlewareError(
151
+ f"Middleware processing failed: {str(e)}",
152
+ error_code=-32021
153
+ )
154
+
155
+ def _process_request(self, environ: Dict[str, Any], start_response,
156
+ flask_request: Request, auth_result: AuthResult = None) -> List[bytes]:
157
+ """
158
+ Process the actual request through the WSGI application.
159
+
160
+ Args:
161
+ environ (Dict[str, Any]): WSGI environment
162
+ start_response: WSGI start_response callable
163
+ flask_request (Request): Flask request object
164
+ auth_result (AuthResult): Authentication result (optional)
165
+
166
+ Returns:
167
+ List[bytes]: WSGI response body
168
+ """
169
+ # Store auth result in request context for later use
170
+ if auth_result:
171
+ flask_request.auth_result = auth_result
172
+
173
+ # Call the original WSGI application
174
+ def custom_start_response(status, headers, exc_info=None):
175
+ # Add security headers
176
+ security_headers = self._get_security_headers()
177
+ headers.extend(security_headers)
178
+
179
+ # Log successful request
180
+ if auth_result:
181
+ self._log_security_event("request_processed", {
182
+ "ip_address": self._get_client_ip(flask_request),
183
+ "username": auth_result.username,
184
+ "path": flask_request.path,
185
+ "method": flask_request.method,
186
+ "status_code": int(status.split()[0])
187
+ })
188
+
189
+ return start_response(status, headers, exc_info)
190
+
191
+ # Get the original WSGI app from the middleware chain
192
+ app = current_app._get_current_object()
193
+ return app(environ, custom_start_response)
194
+
195
+ def _get_rate_limit_identifier(self, request: Request) -> str:
196
+ """
197
+ Get rate limit identifier from Flask request.
198
+
199
+ This method extracts the rate limit identifier from the Flask
200
+ request, typically using the client IP address.
201
+
202
+ Args:
203
+ request (Request): Flask request object
204
+
205
+ Returns:
206
+ str: Rate limit identifier (IP address)
207
+ """
208
+ return self._get_client_ip(request)
209
+
210
+ def _get_request_path(self, request: Request) -> str:
211
+ """
212
+ Get request path from Flask request.
213
+
214
+ Args:
215
+ request (Request): Flask request object
216
+
217
+ Returns:
218
+ str: Request path
219
+ """
220
+ return request.path
221
+
222
+ def _get_required_permissions(self, request: Request) -> List[str]:
223
+ """
224
+ Get required permissions for Flask request.
225
+
226
+ This method extracts required permissions from the Flask request,
227
+ typically from route decorators or request context.
228
+
229
+ Args:
230
+ request (Request): Flask request object
231
+
232
+ Returns:
233
+ List[str]: List of required permissions
234
+ """
235
+ # Try to get permissions from request context
236
+ if hasattr(request, 'required_permissions'):
237
+ return request.required_permissions
238
+
239
+ # Try to get permissions from route decorators
240
+ if hasattr(request, 'endpoint'):
241
+ # Check if endpoint has permission decorators
242
+ endpoint = request.endpoint
243
+ if hasattr(endpoint, 'required_permissions') and endpoint.required_permissions is not None:
244
+ return endpoint.required_permissions
245
+ # Check for permission decorators
246
+ if hasattr(endpoint, '__permissions__') and endpoint.__permissions__ is not None:
247
+ return endpoint.__permissions__
248
+ # Check for role-based decorators
249
+ if hasattr(endpoint, 'required_roles') and endpoint.required_roles is not None:
250
+ return endpoint.required_roles
251
+
252
+ # Default: no specific permissions required
253
+ return []
254
+
255
+ def _try_auth_method(self, request: Request, method: str) -> AuthResult:
256
+ """
257
+ Try authentication using specific method with Flask request.
258
+
259
+ This method attempts to authenticate the Flask request using
260
+ the specified authentication method.
261
+
262
+ Args:
263
+ request (Request): Flask request object
264
+ method (str): Authentication method to try
265
+
266
+ Returns:
267
+ AuthResult: Authentication result
268
+ """
269
+ try:
270
+ if method == "api_key":
271
+ return self._try_api_key_auth(request)
272
+ elif method == "jwt":
273
+ return self._try_jwt_auth(request)
274
+ elif method == "certificate":
275
+ return self._try_certificate_auth(request)
276
+ elif method == "basic":
277
+ return self._try_basic_auth(request)
278
+ else:
279
+ return AuthResult(
280
+ is_valid=False,
281
+ status=AuthStatus.FAILED,
282
+ username=None,
283
+ roles=[],
284
+ auth_method=None,
285
+ error_code=-32022,
286
+ error_message=f"Unsupported authentication method: {method}"
287
+ )
288
+ except Exception as e:
289
+ self.logger.error(
290
+ f"Authentication method {method} failed",
291
+ extra={"error": str(e)},
292
+ exc_info=True
293
+ )
294
+ return AuthResult(
295
+ is_valid=False,
296
+ status=AuthStatus.FAILED,
297
+ username=None,
298
+ roles=[],
299
+ auth_method=None,
300
+ error_code=-32023,
301
+ error_message=f"Authentication method {method} failed: {str(e)}"
302
+ )
303
+
304
+ def _try_api_key_auth(self, request: Request) -> AuthResult:
305
+ """
306
+ Try API key authentication with Flask request.
307
+
308
+ Args:
309
+ request (Request): Flask request object
310
+
311
+ Returns:
312
+ AuthResult: Authentication result
313
+ """
314
+ # Try to get API key from headers
315
+ api_key = request.headers.get("X-API-Key")
316
+ if not api_key:
317
+ # Try Authorization header
318
+ auth_header = request.headers.get("Authorization")
319
+ if auth_header and auth_header.startswith("Bearer "):
320
+ api_key = auth_header[7:] # Remove "Bearer " prefix
321
+
322
+ if not api_key:
323
+ return AuthResult(
324
+ is_valid=False,
325
+ status=AuthStatus.FAILED,
326
+ username=None,
327
+ roles=[],
328
+ auth_method=AuthMethod.API_KEY,
329
+ error_code=-32024,
330
+ error_message="API key not found in request"
331
+ )
332
+
333
+ # Authenticate using security manager
334
+ return self.security_manager.auth_manager.authenticate_api_key(api_key)
335
+
336
+ def _try_jwt_auth(self, request: Request) -> AuthResult:
337
+ """
338
+ Try JWT authentication with Flask request.
339
+
340
+ Args:
341
+ request (Request): Flask request object
342
+
343
+ Returns:
344
+ AuthResult: Authentication result
345
+ """
346
+ # Try to get JWT token from Authorization header
347
+ auth_header = request.headers.get("Authorization")
348
+ if not auth_header or not auth_header.startswith("Bearer "):
349
+ return AuthResult(
350
+ is_valid=False,
351
+ status=AuthStatus.FAILED,
352
+ username=None,
353
+ roles=[],
354
+ auth_method=AuthMethod.JWT,
355
+ error_code=-32025,
356
+ error_message="JWT token not found in Authorization header"
357
+ )
358
+
359
+ token = auth_header[7:] # Remove "Bearer " prefix
360
+
361
+ # Authenticate using security manager
362
+ return self.security_manager.auth_manager.authenticate_jwt_token(token)
363
+
364
+ def _try_certificate_auth(self, request: Request) -> AuthResult:
365
+ """
366
+ Try certificate authentication with Flask request.
367
+
368
+ Args:
369
+ request (Request): Flask request object
370
+
371
+ Returns:
372
+ AuthResult: Authentication result
373
+ """
374
+ # For certificate authentication, we would typically need
375
+ # to access the client certificate from the SSL context
376
+ # This is more complex and depends on the SSL configuration
377
+
378
+ # For now, return not implemented
379
+ return AuthResult(
380
+ is_valid=False,
381
+ status=AuthStatus.FAILED,
382
+ username=None,
383
+ roles=[],
384
+ auth_method=AuthMethod.CERTIFICATE,
385
+ error_code=-32026,
386
+ error_message="Certificate authentication not implemented"
387
+ )
388
+
389
+ def _try_basic_auth(self, request: Request) -> AuthResult:
390
+ """
391
+ Try basic authentication with Flask request.
392
+
393
+ Args:
394
+ request (Request): Flask request object
395
+
396
+ Returns:
397
+ AuthResult: Authentication result
398
+ """
399
+ # Try to get basic auth from Authorization header
400
+ auth_header = request.headers.get("Authorization")
401
+ if not auth_header or not auth_header.startswith("Basic "):
402
+ return AuthResult(
403
+ is_valid=False,
404
+ status=AuthStatus.FAILED,
405
+ username=None,
406
+ roles=[],
407
+ auth_method=AuthMethod.BASIC,
408
+ error_code=-32027,
409
+ error_message="Basic authentication credentials not found"
410
+ )
411
+
412
+ # Basic auth implementation would go here
413
+ # For now, return not implemented
414
+ return AuthResult(
415
+ is_valid=False,
416
+ status=AuthStatus.FAILED,
417
+ username=None,
418
+ roles=[],
419
+ auth_method=AuthMethod.BASIC,
420
+ error_code=-32028,
421
+ error_message="Basic authentication not implemented"
422
+ )
423
+
424
+ def _apply_security_headers(self, response: Response, headers: Dict[str, str]) -> None:
425
+ """
426
+ Apply security headers to Flask response.
427
+
428
+ Args:
429
+ response (Response): Flask response object
430
+ headers (Dict[str, str]): Headers to apply
431
+ """
432
+ for header_name, header_value in headers.items():
433
+ response.headers[header_name] = header_value
434
+
435
+ def _create_error_response(self, status_code: int, message: str) -> Response:
436
+ """
437
+ Create error response for security violations.
438
+
439
+ Args:
440
+ status_code (int): HTTP status code
441
+ message (str): Error message
442
+
443
+ Returns:
444
+ Response: Flask error response
445
+ """
446
+ return make_response(
447
+ jsonify({
448
+ "error": "Security violation",
449
+ "message": message,
450
+ "error_code": -32029
451
+ }),
452
+ status_code
453
+ )
454
+
455
+ def _rate_limit_response(self, start_response) -> List[bytes]:
456
+ """
457
+ Create rate limit exceeded response.
458
+
459
+ Args:
460
+ start_response: WSGI start_response callable
461
+
462
+ Returns:
463
+ List[bytes]: WSGI response body
464
+ """
465
+ response_data = {
466
+ "error": "Rate limit exceeded",
467
+ "message": "Too many requests, please try again later",
468
+ "error_code": -32030
469
+ }
470
+
471
+ response_body = json.dumps(response_data).encode('utf-8')
472
+ headers = [
473
+ ('Content-Type', 'application/json'),
474
+ ('Content-Length', str(len(response_body))),
475
+ ('Retry-After', str(self.config.rate_limit.window_size_seconds))
476
+ ]
477
+
478
+ start_response('429 Too Many Requests', headers)
479
+ return [response_body]
480
+
481
+ def _auth_error_response(self, auth_result: AuthResult, start_response) -> List[bytes]:
482
+ """
483
+ Create authentication error response.
484
+
485
+ Args:
486
+ auth_result (AuthResult): Authentication result
487
+ start_response: WSGI start_response callable
488
+
489
+ Returns:
490
+ List[bytes]: WSGI response body
491
+ """
492
+ response_data = {
493
+ "error": "Authentication failed",
494
+ "message": auth_result.error_message or "Invalid credentials",
495
+ "error_code": auth_result.error_code,
496
+ "auth_method": auth_result.auth_method
497
+ }
498
+
499
+ response_body = json.dumps(response_data).encode('utf-8')
500
+ headers = [
501
+ ('Content-Type', 'application/json'),
502
+ ('Content-Length', str(len(response_body))),
503
+ ('WWW-Authenticate', 'Bearer, ApiKey')
504
+ ]
505
+
506
+ start_response('401 Unauthorized', headers)
507
+ return [response_body]
508
+
509
+ def _permission_error_response(self, start_response) -> List[bytes]:
510
+ """
511
+ Create permission denied response.
512
+
513
+ Args:
514
+ start_response: WSGI start_response callable
515
+
516
+ Returns:
517
+ List[bytes]: WSGI response body
518
+ """
519
+ response_data = {
520
+ "error": "Permission denied",
521
+ "message": "Insufficient permissions to access this resource",
522
+ "error_code": -32031
523
+ }
524
+
525
+ response_body = json.dumps(response_data).encode('utf-8')
526
+ headers = [
527
+ ('Content-Type', 'application/json'),
528
+ ('Content-Length', str(len(response_body)))
529
+ ]
530
+
531
+ start_response('403 Forbidden', headers)
532
+ return [response_body]
533
+
534
+ def _get_client_ip(self, request: Request) -> str:
535
+ """
536
+ Get client IP address from Flask request.
537
+
538
+ Args:
539
+ request (Request): Flask request object
540
+
541
+ Returns:
542
+ str: Client IP address
543
+ """
544
+ # Try to get IP from X-Forwarded-For header (for proxy scenarios)
545
+ forwarded_for = request.headers.get("X-Forwarded-For")
546
+ if forwarded_for:
547
+ # Take the first IP in the chain
548
+ return forwarded_for.split(",")[0].strip()
549
+
550
+ # Try to get IP from X-Real-IP header
551
+ real_ip = request.headers.get("X-Real-IP")
552
+ if real_ip:
553
+ return real_ip
554
+
555
+ # Fall back to remote address
556
+ if request.remote_addr:
557
+ return request.remote_addr
558
+
559
+ # Default fallback
560
+ # Fallback to default IP from config or environment
561
+ default_ip = getattr(self.config, 'default_client_ip', None)
562
+ if default_ip:
563
+ return default_ip
564
+
565
+ # Use environment variable or default
566
+ import os
567
+ from ..constants import DEFAULT_CLIENT_IP
568
+ return os.environ.get('DEFAULT_CLIENT_IP', DEFAULT_CLIENT_IP)
569
+
570
+ def _get_security_headers(self) -> List[tuple]:
571
+ """
572
+ Get security headers to add to responses.
573
+
574
+ Returns:
575
+ List[tuple]: List of (header_name, header_value) tuples
576
+ """
577
+ headers = [
578
+ ('X-Content-Type-Options', 'nosniff'),
579
+ ('X-Frame-Options', 'DENY'),
580
+ ('X-XSS-Protection', '1; mode=block'),
581
+ ('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'),
582
+ ('Content-Security-Policy', 'default-src \'self\''),
583
+ ('Referrer-Policy', 'strict-origin-when-cross-origin')
584
+ ]
585
+
586
+ # Add custom security headers from config
587
+ if self.config.auth and self.config.auth.security_headers:
588
+ for header_name, header_value in self.config.auth.security_headers.items():
589
+ headers.append((header_name, header_value))
590
+
591
+ return headers