Coverage for mcpgateway / utils / security_cookies.py: 100%
46 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
1# -*- coding: utf-8 -*-
2"""Location: ./mcpgateway/utils/security_cookies.py
3Copyright 2025
4SPDX-License-Identifier: Apache-2.0
5Authors: Mihai Criveti
7Security Cookie Utilities for MCP Gateway.
9This module provides utilities for setting secure authentication cookies with proper
10security attributes to prevent common cookie-based attacks.
11"""
13# Standard
14import logging
16# Third-Party
17from fastapi import Response
19# First-Party
20from mcpgateway.config import settings
22logger = logging.getLogger(__name__)
24# RFC 6265 §6.1: browsers SHOULD support cookies of at least 4096 bytes
25_COOKIE_HARD_LIMIT = 4096
26_COOKIE_WARN_THRESHOLD = 3800
29class CookieTooLargeError(Exception):
30 """Raised when a cookie value exceeds the browser's 4KB limit."""
32 def __init__(self, cookie_size: int, limit: int = _COOKIE_HARD_LIMIT):
33 """Initialize with the actual cookie size and the browser limit.
35 Args:
36 cookie_size: Actual size of the cookie in bytes.
37 limit: Maximum allowed cookie size in bytes.
38 """
39 self.cookie_size = cookie_size
40 self.limit = limit
41 super().__init__(f"Cookie size {cookie_size} bytes exceeds browser limit of {limit} bytes")
44def set_auth_cookie(response: Response, token: str, remember_me: bool = False) -> None:
45 """
46 Set authentication cookie with security flags and size validation.
48 Configures the JWT token as a secure HTTP-only cookie with appropriate
49 security attributes to prevent XSS and CSRF attacks.
51 Args:
52 response: FastAPI response object to set the cookie on
53 token: JWT token to store in the cookie
54 remember_me: If True, sets longer expiration time (30 days vs 1 hour)
56 Raises:
57 CookieTooLargeError: If the cookie would exceed 4096 bytes
59 Security attributes set:
60 - httponly: Prevents JavaScript access to the cookie
61 - secure: HTTPS only in production environments
62 - samesite: CSRF protection (configurable, defaults to 'lax')
63 - path: Cookie scope limitation
64 - max_age: Automatic expiration
66 Examples:
67 Basic cookie set with remember_me disabled:
68 >>> from fastapi import Response
69 >>> from mcpgateway.utils.security_cookies import set_auth_cookie
70 >>> resp = Response()
71 >>> set_auth_cookie(resp, 'tok123', remember_me=False)
72 >>> header = resp.headers.get('set-cookie')
73 >>> 'jwt_token=' in header and 'HttpOnly' in header and 'Path=/' in header
74 True
76 Extended expiration when remember_me is True:
77 >>> resp2 = Response()
78 >>> set_auth_cookie(resp2, 'tok123', remember_me=True)
79 >>> 'Max-Age=2592000' in resp2.headers.get('set-cookie') # 30 days
80 True
81 """
82 # Set expiration based on remember_me preference
83 max_age = 30 * 24 * 3600 if remember_me else 3600 # 30 days or 1 hour
85 # Determine if we should use secure flag
86 # In production or when explicitly configured, require HTTPS
87 use_secure = (settings.environment == "production") or settings.secure_cookies
88 samesite = settings.cookie_samesite
89 path = settings.app_root_path or "/"
91 # Estimate cookie size in bytes (Set-Cookie header format)
92 # The cookie name, value, and attributes all count toward the limit
93 cookie_header = f"jwt_token={token}; HttpOnly; SameSite={samesite}; Path={path}; Max-Age={max_age}"
94 if use_secure:
95 cookie_header += "; Secure"
96 cookie_size = len(cookie_header.encode("ascii", errors="replace"))
98 # Extract sub claim for log context (best-effort, don't build control flow on it)
99 sub_for_log = ""
100 try:
101 # Standard
102 import base64
103 import json
105 parts = token.split(".")
106 if len(parts) >= 2:
107 padded = parts[1] + "=" * (-len(parts[1]) % 4)
108 payload = json.loads(base64.urlsafe_b64decode(padded))
109 sub_for_log = payload.get("sub", "")
110 except Exception: # nosec B110 - Best-effort sub extraction for logging
111 pass
113 if cookie_size > _COOKIE_HARD_LIMIT:
114 logger.error("JWT cookie size %d bytes exceeds %d byte browser limit (user: %s)", cookie_size, _COOKIE_HARD_LIMIT, sub_for_log)
115 raise CookieTooLargeError(cookie_size)
117 if cookie_size > _COOKIE_WARN_THRESHOLD:
118 logger.warning("JWT cookie size %d bytes approaching %d byte browser limit (user: %s)", cookie_size, _COOKIE_HARD_LIMIT, sub_for_log)
120 response.set_cookie(
121 key="jwt_token",
122 value=token,
123 max_age=max_age,
124 httponly=True, # Prevents JavaScript access
125 secure=use_secure, # HTTPS only in production
126 samesite=samesite, # CSRF protection
127 path=path, # Cookie scope
128 )
131def clear_auth_cookie(response: Response) -> None:
132 """
133 Clear authentication cookie securely.
135 Removes the JWT token cookie by setting it to expire immediately
136 with the same security attributes used when setting it.
138 Args:
139 response: FastAPI response object to clear the cookie from
141 Examples:
142 >>> from fastapi import Response
143 >>> resp = Response()
144 >>> set_auth_cookie(resp, 'tok123')
145 >>> clear_auth_cookie(resp)
146 >>> # Deletion sets another Set-Cookie for jwt_token; presence indicates cleared cookie header
147 >>> 'jwt_token=' in resp.headers.get('set-cookie')
148 True
149 """
150 # Use same security settings as when setting the cookie
151 use_secure = (settings.environment == "production") or settings.secure_cookies
153 response.delete_cookie(
154 key="jwt_token",
155 path=settings.app_root_path or "/",
156 secure=use_secure,
157 httponly=True,
158 samesite=settings.cookie_samesite,
159 )
162def set_session_cookie(response: Response, session_id: str, max_age: int = 3600) -> None:
163 """
164 Set session cookie with security flags.
166 Configures a session ID cookie with appropriate security attributes.
168 Args:
169 response: FastAPI response object to set the cookie on
170 session_id: Session identifier to store in the cookie
171 max_age: Cookie expiration time in seconds (default: 1 hour)
173 Examples:
174 >>> from fastapi import Response
175 >>> resp = Response()
176 >>> set_session_cookie(resp, 'sess-1', max_age=3600)
177 >>> header = resp.headers.get('set-cookie')
178 >>> 'session_id=sess-1' in header and 'HttpOnly' in header
179 True
180 """
181 use_secure = (settings.environment == "production") or settings.secure_cookies
183 response.set_cookie(
184 key="session_id",
185 value=session_id,
186 max_age=max_age,
187 httponly=True,
188 secure=use_secure,
189 samesite=settings.cookie_samesite,
190 path=settings.app_root_path or "/",
191 )
194def clear_session_cookie(response: Response) -> None:
195 """
196 Clear session cookie securely.
198 Args:
199 response: FastAPI response object to clear the cookie from
201 Examples:
202 >>> from fastapi import Response
203 >>> resp = Response()
204 >>> set_session_cookie(resp, 'sess-2', max_age=60)
205 >>> clear_session_cookie(resp)
206 >>> 'session_id=' in resp.headers.get('set-cookie')
207 True
208 """
209 use_secure = (settings.environment == "production") or settings.secure_cookies
211 response.delete_cookie(
212 key="session_id",
213 path=settings.app_root_path or "/",
214 secure=use_secure,
215 httponly=True,
216 samesite=settings.cookie_samesite,
217 )