aiptx 2.0.2__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.
Potentially problematic release.
This version of aiptx might be problematic. Click here for more details.
- aipt_v2/__init__.py +110 -0
- aipt_v2/__main__.py +24 -0
- aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
- aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
- aipt_v2/agents/__init__.py +24 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +960 -0
- aipt_v2/browser/__init__.py +31 -0
- aipt_v2/browser/automation.py +458 -0
- aipt_v2/browser/crawler.py +453 -0
- aipt_v2/cli.py +321 -0
- aipt_v2/compliance/__init__.py +71 -0
- aipt_v2/compliance/compliance_report.py +449 -0
- aipt_v2/compliance/framework_mapper.py +424 -0
- aipt_v2/compliance/nist_mapping.py +345 -0
- aipt_v2/compliance/owasp_mapping.py +330 -0
- aipt_v2/compliance/pci_mapping.py +297 -0
- aipt_v2/config.py +288 -0
- aipt_v2/core/__init__.py +43 -0
- aipt_v2/core/agent.py +630 -0
- aipt_v2/core/llm.py +395 -0
- aipt_v2/core/memory.py +305 -0
- aipt_v2/core/ptt.py +329 -0
- aipt_v2/database/__init__.py +14 -0
- aipt_v2/database/models.py +232 -0
- aipt_v2/database/repository.py +384 -0
- aipt_v2/docker/__init__.py +23 -0
- aipt_v2/docker/builder.py +260 -0
- aipt_v2/docker/manager.py +222 -0
- aipt_v2/docker/sandbox.py +371 -0
- aipt_v2/evasion/__init__.py +58 -0
- aipt_v2/evasion/request_obfuscator.py +272 -0
- aipt_v2/evasion/tls_fingerprint.py +285 -0
- aipt_v2/evasion/ua_rotator.py +301 -0
- aipt_v2/evasion/waf_bypass.py +439 -0
- aipt_v2/execution/__init__.py +23 -0
- aipt_v2/execution/executor.py +302 -0
- aipt_v2/execution/parser.py +544 -0
- aipt_v2/execution/terminal.py +337 -0
- aipt_v2/health.py +437 -0
- aipt_v2/intelligence/__init__.py +85 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/rag.py +239 -0
- aipt_v2/intelligence/scope.py +442 -0
- aipt_v2/intelligence/searchers/__init__.py +5 -0
- aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
- aipt_v2/intelligence/searchers/github_searcher.py +467 -0
- aipt_v2/intelligence/searchers/google_searcher.py +281 -0
- aipt_v2/intelligence/tools.json +443 -0
- aipt_v2/intelligence/triage.py +670 -0
- aipt_v2/interface/__init__.py +5 -0
- aipt_v2/interface/cli.py +230 -0
- aipt_v2/interface/main.py +501 -0
- aipt_v2/interface/tui.py +1276 -0
- aipt_v2/interface/utils.py +583 -0
- aipt_v2/llm/__init__.py +39 -0
- aipt_v2/llm/config.py +26 -0
- aipt_v2/llm/llm.py +514 -0
- aipt_v2/llm/memory.py +214 -0
- aipt_v2/llm/request_queue.py +89 -0
- aipt_v2/llm/utils.py +89 -0
- aipt_v2/models/__init__.py +15 -0
- aipt_v2/models/findings.py +295 -0
- aipt_v2/models/phase_result.py +224 -0
- aipt_v2/models/scan_config.py +207 -0
- aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
- aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
- aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
- aipt_v2/monitoring/prometheus.yml +60 -0
- aipt_v2/orchestration/__init__.py +52 -0
- aipt_v2/orchestration/pipeline.py +398 -0
- aipt_v2/orchestration/progress.py +300 -0
- aipt_v2/orchestration/scheduler.py +296 -0
- aipt_v2/orchestrator.py +2284 -0
- aipt_v2/payloads/__init__.py +27 -0
- aipt_v2/payloads/cmdi.py +150 -0
- aipt_v2/payloads/sqli.py +263 -0
- aipt_v2/payloads/ssrf.py +204 -0
- aipt_v2/payloads/templates.py +222 -0
- aipt_v2/payloads/traversal.py +166 -0
- aipt_v2/payloads/xss.py +204 -0
- aipt_v2/prompts/__init__.py +60 -0
- aipt_v2/proxy/__init__.py +29 -0
- aipt_v2/proxy/history.py +352 -0
- aipt_v2/proxy/interceptor.py +452 -0
- aipt_v2/recon/__init__.py +44 -0
- aipt_v2/recon/dns.py +241 -0
- aipt_v2/recon/osint.py +367 -0
- aipt_v2/recon/subdomain.py +372 -0
- aipt_v2/recon/tech_detect.py +311 -0
- aipt_v2/reports/__init__.py +17 -0
- aipt_v2/reports/generator.py +313 -0
- aipt_v2/reports/html_report.py +378 -0
- aipt_v2/runtime/__init__.py +44 -0
- aipt_v2/runtime/base.py +30 -0
- aipt_v2/runtime/docker.py +401 -0
- aipt_v2/runtime/local.py +346 -0
- aipt_v2/runtime/tool_server.py +205 -0
- aipt_v2/scanners/__init__.py +28 -0
- aipt_v2/scanners/base.py +273 -0
- aipt_v2/scanners/nikto.py +244 -0
- aipt_v2/scanners/nmap.py +402 -0
- aipt_v2/scanners/nuclei.py +273 -0
- aipt_v2/scanners/web.py +454 -0
- aipt_v2/scripts/security_audit.py +366 -0
- aipt_v2/telemetry/__init__.py +7 -0
- aipt_v2/telemetry/tracer.py +347 -0
- aipt_v2/terminal/__init__.py +28 -0
- aipt_v2/terminal/executor.py +400 -0
- aipt_v2/terminal/sandbox.py +350 -0
- aipt_v2/tools/__init__.py +44 -0
- aipt_v2/tools/active_directory/__init__.py +78 -0
- aipt_v2/tools/active_directory/ad_config.py +238 -0
- aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
- aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
- aipt_v2/tools/active_directory/ldap_enum.py +533 -0
- aipt_v2/tools/active_directory/smb_attacks.py +505 -0
- aipt_v2/tools/agents_graph/__init__.py +19 -0
- aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
- aipt_v2/tools/api_security/__init__.py +76 -0
- aipt_v2/tools/api_security/api_discovery.py +608 -0
- aipt_v2/tools/api_security/graphql_scanner.py +622 -0
- aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
- aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
- aipt_v2/tools/browser/__init__.py +5 -0
- aipt_v2/tools/browser/browser_actions.py +238 -0
- aipt_v2/tools/browser/browser_instance.py +535 -0
- aipt_v2/tools/browser/tab_manager.py +344 -0
- aipt_v2/tools/cloud/__init__.py +70 -0
- aipt_v2/tools/cloud/cloud_config.py +273 -0
- aipt_v2/tools/cloud/cloud_scanner.py +639 -0
- aipt_v2/tools/cloud/prowler_tool.py +571 -0
- aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
- aipt_v2/tools/executor.py +307 -0
- aipt_v2/tools/parser.py +408 -0
- aipt_v2/tools/proxy/__init__.py +5 -0
- aipt_v2/tools/proxy/proxy_actions.py +103 -0
- aipt_v2/tools/proxy/proxy_manager.py +789 -0
- aipt_v2/tools/registry.py +196 -0
- aipt_v2/tools/scanners/__init__.py +343 -0
- aipt_v2/tools/scanners/acunetix_tool.py +712 -0
- aipt_v2/tools/scanners/burp_tool.py +631 -0
- aipt_v2/tools/scanners/config.py +156 -0
- aipt_v2/tools/scanners/nessus_tool.py +588 -0
- aipt_v2/tools/scanners/zap_tool.py +612 -0
- aipt_v2/tools/terminal/__init__.py +5 -0
- aipt_v2/tools/terminal/terminal_actions.py +37 -0
- aipt_v2/tools/terminal/terminal_manager.py +153 -0
- aipt_v2/tools/terminal/terminal_session.py +449 -0
- aipt_v2/tools/tool_processing.py +108 -0
- aipt_v2/utils/__init__.py +17 -0
- aipt_v2/utils/logging.py +201 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aiptx-2.0.2.dist-info/METADATA +324 -0
- aiptx-2.0.2.dist-info/RECORD +165 -0
- aiptx-2.0.2.dist-info/WHEEL +5 -0
- aiptx-2.0.2.dist-info/entry_points.txt +7 -0
- aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Authenticated Scanning Support
|
|
3
|
+
|
|
4
|
+
Provides authentication mechanisms for scanning protected resources.
|
|
5
|
+
Supports multiple authentication methods:
|
|
6
|
+
- Session cookies
|
|
7
|
+
- Bearer tokens (JWT, OAuth)
|
|
8
|
+
- Basic authentication
|
|
9
|
+
- API keys
|
|
10
|
+
- Custom headers
|
|
11
|
+
- Form-based login automation
|
|
12
|
+
|
|
13
|
+
This enables testing authenticated portions of applications
|
|
14
|
+
(with proper authorization from the client).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import base64
|
|
20
|
+
import hashlib
|
|
21
|
+
import hmac
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import re
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from datetime import datetime, timedelta
|
|
28
|
+
from enum import Enum
|
|
29
|
+
from typing import Any, Callable
|
|
30
|
+
from urllib.parse import urlencode
|
|
31
|
+
|
|
32
|
+
import httpx
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AuthMethod(Enum):
|
|
39
|
+
"""Authentication methods supported"""
|
|
40
|
+
NONE = "none"
|
|
41
|
+
COOKIE = "cookie"
|
|
42
|
+
BEARER_TOKEN = "bearer_token"
|
|
43
|
+
BASIC_AUTH = "basic_auth"
|
|
44
|
+
API_KEY = "api_key"
|
|
45
|
+
CUSTOM_HEADER = "custom_header"
|
|
46
|
+
FORM_LOGIN = "form_login"
|
|
47
|
+
OAUTH2 = "oauth2"
|
|
48
|
+
AWS_SIGV4 = "aws_sigv4"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class AuthCredentials:
|
|
53
|
+
"""Authentication credentials"""
|
|
54
|
+
method: AuthMethod = AuthMethod.NONE
|
|
55
|
+
|
|
56
|
+
# For COOKIE method
|
|
57
|
+
cookies: dict[str, str] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
# For BEARER_TOKEN method
|
|
60
|
+
token: str = ""
|
|
61
|
+
token_prefix: str = "Bearer"
|
|
62
|
+
|
|
63
|
+
# For BASIC_AUTH method
|
|
64
|
+
username: str = ""
|
|
65
|
+
password: str = ""
|
|
66
|
+
|
|
67
|
+
# For API_KEY method
|
|
68
|
+
api_key: str = ""
|
|
69
|
+
api_key_header: str = "X-API-Key"
|
|
70
|
+
api_key_in_query: bool = False
|
|
71
|
+
api_key_query_param: str = "api_key"
|
|
72
|
+
|
|
73
|
+
# For CUSTOM_HEADER method
|
|
74
|
+
custom_headers: dict[str, str] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
# For FORM_LOGIN method
|
|
77
|
+
login_url: str = ""
|
|
78
|
+
login_data: dict[str, str] = field(default_factory=dict)
|
|
79
|
+
csrf_field: str = "" # If CSRF token needed
|
|
80
|
+
success_indicator: str = "" # Text/pattern indicating successful login
|
|
81
|
+
|
|
82
|
+
# For OAUTH2 method
|
|
83
|
+
oauth_client_id: str = ""
|
|
84
|
+
oauth_client_secret: str = ""
|
|
85
|
+
oauth_token_url: str = ""
|
|
86
|
+
oauth_scope: str = ""
|
|
87
|
+
|
|
88
|
+
# Token management
|
|
89
|
+
token_expires_at: datetime | None = None
|
|
90
|
+
refresh_token: str = ""
|
|
91
|
+
auto_refresh: bool = True
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
return {
|
|
95
|
+
"method": self.method.value,
|
|
96
|
+
"has_cookies": bool(self.cookies),
|
|
97
|
+
"has_token": bool(self.token),
|
|
98
|
+
"has_basic_auth": bool(self.username),
|
|
99
|
+
"has_api_key": bool(self.api_key),
|
|
100
|
+
"has_custom_headers": bool(self.custom_headers),
|
|
101
|
+
"login_url": self.login_url if self.login_url else None,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class AuthSession:
|
|
107
|
+
"""An authenticated session"""
|
|
108
|
+
credentials: AuthCredentials
|
|
109
|
+
session_id: str
|
|
110
|
+
created_at: datetime = field(default_factory=datetime.utcnow)
|
|
111
|
+
last_used: datetime = field(default_factory=datetime.utcnow)
|
|
112
|
+
is_valid: bool = True
|
|
113
|
+
validation_url: str = ""
|
|
114
|
+
validation_indicator: str = ""
|
|
115
|
+
|
|
116
|
+
# Session state
|
|
117
|
+
cookies: dict[str, str] = field(default_factory=dict)
|
|
118
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
119
|
+
|
|
120
|
+
# Statistics
|
|
121
|
+
requests_made: int = 0
|
|
122
|
+
auth_failures: int = 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class AuthenticationManager:
|
|
126
|
+
"""
|
|
127
|
+
Manages authentication for scanning sessions.
|
|
128
|
+
|
|
129
|
+
Handles:
|
|
130
|
+
- Multiple authentication methods
|
|
131
|
+
- Token refresh
|
|
132
|
+
- Session validation
|
|
133
|
+
- Header/cookie injection
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
creds = AuthCredentials(
|
|
137
|
+
method=AuthMethod.BEARER_TOKEN,
|
|
138
|
+
token="eyJhbGciOi...",
|
|
139
|
+
)
|
|
140
|
+
auth_mgr = AuthenticationManager(creds)
|
|
141
|
+
headers = await auth_mgr.get_auth_headers()
|
|
142
|
+
# Use headers in your requests
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, credentials: AuthCredentials):
|
|
146
|
+
self.credentials = credentials
|
|
147
|
+
self._session: AuthSession | None = None
|
|
148
|
+
self._http_client: httpx.AsyncClient | None = None
|
|
149
|
+
self._token_lock = asyncio.Lock()
|
|
150
|
+
|
|
151
|
+
async def initialize(self) -> AuthSession:
|
|
152
|
+
"""
|
|
153
|
+
Initialize authentication session.
|
|
154
|
+
|
|
155
|
+
For form-based login, this will perform the login.
|
|
156
|
+
For OAuth, this will obtain tokens.
|
|
157
|
+
"""
|
|
158
|
+
session_id = hashlib.md5(
|
|
159
|
+
f"{self.credentials.method.value}-{datetime.utcnow().isoformat()}".encode()
|
|
160
|
+
).hexdigest()[:12]
|
|
161
|
+
|
|
162
|
+
self._session = AuthSession(
|
|
163
|
+
credentials=self.credentials,
|
|
164
|
+
session_id=session_id,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Perform initial authentication based on method
|
|
168
|
+
if self.credentials.method == AuthMethod.FORM_LOGIN:
|
|
169
|
+
await self._perform_form_login()
|
|
170
|
+
elif self.credentials.method == AuthMethod.OAUTH2:
|
|
171
|
+
await self._obtain_oauth_token()
|
|
172
|
+
|
|
173
|
+
# Build initial headers
|
|
174
|
+
self._session.headers = await self._build_auth_headers()
|
|
175
|
+
self._session.cookies = self.credentials.cookies.copy()
|
|
176
|
+
|
|
177
|
+
logger.info(f"Auth session initialized: {session_id} ({self.credentials.method.value})")
|
|
178
|
+
return self._session
|
|
179
|
+
|
|
180
|
+
async def get_auth_headers(self) -> dict[str, str]:
|
|
181
|
+
"""
|
|
182
|
+
Get authentication headers for a request.
|
|
183
|
+
|
|
184
|
+
Automatically refreshes tokens if needed.
|
|
185
|
+
"""
|
|
186
|
+
if not self._session:
|
|
187
|
+
await self.initialize()
|
|
188
|
+
|
|
189
|
+
# Check if token needs refresh
|
|
190
|
+
if self.credentials.auto_refresh and self._token_expired():
|
|
191
|
+
async with self._token_lock:
|
|
192
|
+
if self._token_expired(): # Double-check after acquiring lock
|
|
193
|
+
await self._refresh_token()
|
|
194
|
+
|
|
195
|
+
self._session.last_used = datetime.utcnow()
|
|
196
|
+
self._session.requests_made += 1
|
|
197
|
+
|
|
198
|
+
return self._session.headers.copy()
|
|
199
|
+
|
|
200
|
+
async def get_auth_cookies(self) -> dict[str, str]:
|
|
201
|
+
"""Get authentication cookies"""
|
|
202
|
+
if not self._session:
|
|
203
|
+
await self.initialize()
|
|
204
|
+
|
|
205
|
+
return self._session.cookies.copy()
|
|
206
|
+
|
|
207
|
+
async def _build_auth_headers(self) -> dict[str, str]:
|
|
208
|
+
"""Build authentication headers based on method"""
|
|
209
|
+
headers = {}
|
|
210
|
+
|
|
211
|
+
if self.credentials.method == AuthMethod.BEARER_TOKEN:
|
|
212
|
+
headers["Authorization"] = f"{self.credentials.token_prefix} {self.credentials.token}"
|
|
213
|
+
|
|
214
|
+
elif self.credentials.method == AuthMethod.BASIC_AUTH:
|
|
215
|
+
credentials = f"{self.credentials.username}:{self.credentials.password}"
|
|
216
|
+
encoded = base64.b64encode(credentials.encode()).decode()
|
|
217
|
+
headers["Authorization"] = f"Basic {encoded}"
|
|
218
|
+
|
|
219
|
+
elif self.credentials.method == AuthMethod.API_KEY:
|
|
220
|
+
if not self.credentials.api_key_in_query:
|
|
221
|
+
headers[self.credentials.api_key_header] = self.credentials.api_key
|
|
222
|
+
|
|
223
|
+
elif self.credentials.method == AuthMethod.CUSTOM_HEADER:
|
|
224
|
+
headers.update(self.credentials.custom_headers)
|
|
225
|
+
|
|
226
|
+
elif self.credentials.method == AuthMethod.OAUTH2:
|
|
227
|
+
if self.credentials.token:
|
|
228
|
+
headers["Authorization"] = f"Bearer {self.credentials.token}"
|
|
229
|
+
|
|
230
|
+
return headers
|
|
231
|
+
|
|
232
|
+
async def _perform_form_login(self) -> None:
|
|
233
|
+
"""Perform form-based login"""
|
|
234
|
+
if not self.credentials.login_url:
|
|
235
|
+
raise ValueError("login_url required for FORM_LOGIN")
|
|
236
|
+
|
|
237
|
+
client = await self._get_http_client()
|
|
238
|
+
|
|
239
|
+
# Get login page (for CSRF token if needed)
|
|
240
|
+
login_data = self.credentials.login_data.copy()
|
|
241
|
+
|
|
242
|
+
if self.credentials.csrf_field:
|
|
243
|
+
# Fetch login page to get CSRF token
|
|
244
|
+
response = await client.get(self.credentials.login_url)
|
|
245
|
+
csrf_token = self._extract_csrf_token(response.text, self.credentials.csrf_field)
|
|
246
|
+
if csrf_token:
|
|
247
|
+
login_data[self.credentials.csrf_field] = csrf_token
|
|
248
|
+
|
|
249
|
+
# Perform login
|
|
250
|
+
response = await client.post(
|
|
251
|
+
self.credentials.login_url,
|
|
252
|
+
data=login_data,
|
|
253
|
+
follow_redirects=True,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Check for success
|
|
257
|
+
if self.credentials.success_indicator:
|
|
258
|
+
if self.credentials.success_indicator not in response.text:
|
|
259
|
+
raise AuthenticationError(
|
|
260
|
+
f"Login failed - success indicator not found: {self.credentials.success_indicator}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Extract session cookies
|
|
264
|
+
for cookie in client.cookies.jar:
|
|
265
|
+
self._session.cookies[cookie.name] = cookie.value
|
|
266
|
+
|
|
267
|
+
logger.info("Form login successful")
|
|
268
|
+
|
|
269
|
+
def _extract_csrf_token(self, html: str, field_name: str) -> str | None:
|
|
270
|
+
"""Extract CSRF token from HTML"""
|
|
271
|
+
# Try common patterns
|
|
272
|
+
patterns = [
|
|
273
|
+
rf'name="{field_name}"[^>]*value="([^"]+)"',
|
|
274
|
+
rf'name=\'{field_name}\'[^>]*value=\'([^\']+)\'',
|
|
275
|
+
rf'value="([^"]+)"[^>]*name="{field_name}"',
|
|
276
|
+
rf'data-csrf="([^"]+)"',
|
|
277
|
+
rf'csrf[_-]?token["\']?\s*[:=]\s*["\']([^"\']+)',
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
for pattern in patterns:
|
|
281
|
+
match = re.search(pattern, html, re.IGNORECASE)
|
|
282
|
+
if match:
|
|
283
|
+
return match.group(1)
|
|
284
|
+
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
async def _obtain_oauth_token(self) -> None:
|
|
288
|
+
"""Obtain OAuth2 token"""
|
|
289
|
+
if not self.credentials.oauth_token_url:
|
|
290
|
+
raise ValueError("oauth_token_url required for OAUTH2")
|
|
291
|
+
|
|
292
|
+
client = await self._get_http_client()
|
|
293
|
+
|
|
294
|
+
token_data = {
|
|
295
|
+
"grant_type": "client_credentials",
|
|
296
|
+
"client_id": self.credentials.oauth_client_id,
|
|
297
|
+
"client_secret": self.credentials.oauth_client_secret,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if self.credentials.oauth_scope:
|
|
301
|
+
token_data["scope"] = self.credentials.oauth_scope
|
|
302
|
+
|
|
303
|
+
response = await client.post(
|
|
304
|
+
self.credentials.oauth_token_url,
|
|
305
|
+
data=token_data,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if response.status_code != 200:
|
|
309
|
+
raise AuthenticationError(f"OAuth token request failed: {response.status_code}")
|
|
310
|
+
|
|
311
|
+
data = response.json()
|
|
312
|
+
self.credentials.token = data.get("access_token", "")
|
|
313
|
+
self.credentials.refresh_token = data.get("refresh_token", "")
|
|
314
|
+
|
|
315
|
+
expires_in = data.get("expires_in", 3600)
|
|
316
|
+
self.credentials.token_expires_at = datetime.utcnow() + timedelta(seconds=expires_in - 60)
|
|
317
|
+
|
|
318
|
+
logger.info(f"OAuth token obtained, expires in {expires_in}s")
|
|
319
|
+
|
|
320
|
+
def _token_expired(self) -> bool:
|
|
321
|
+
"""Check if current token is expired"""
|
|
322
|
+
if not self.credentials.token_expires_at:
|
|
323
|
+
return False
|
|
324
|
+
return datetime.utcnow() >= self.credentials.token_expires_at
|
|
325
|
+
|
|
326
|
+
async def _refresh_token(self) -> None:
|
|
327
|
+
"""Refresh OAuth token"""
|
|
328
|
+
if self.credentials.method == AuthMethod.OAUTH2 and self.credentials.refresh_token:
|
|
329
|
+
client = await self._get_http_client()
|
|
330
|
+
|
|
331
|
+
token_data = {
|
|
332
|
+
"grant_type": "refresh_token",
|
|
333
|
+
"refresh_token": self.credentials.refresh_token,
|
|
334
|
+
"client_id": self.credentials.oauth_client_id,
|
|
335
|
+
"client_secret": self.credentials.oauth_client_secret,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
response = await client.post(
|
|
339
|
+
self.credentials.oauth_token_url,
|
|
340
|
+
data=token_data,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if response.status_code == 200:
|
|
344
|
+
data = response.json()
|
|
345
|
+
self.credentials.token = data.get("access_token", "")
|
|
346
|
+
|
|
347
|
+
new_refresh = data.get("refresh_token")
|
|
348
|
+
if new_refresh:
|
|
349
|
+
self.credentials.refresh_token = new_refresh
|
|
350
|
+
|
|
351
|
+
expires_in = data.get("expires_in", 3600)
|
|
352
|
+
self.credentials.token_expires_at = datetime.utcnow() + timedelta(seconds=expires_in - 60)
|
|
353
|
+
|
|
354
|
+
# Update session headers
|
|
355
|
+
self._session.headers = await self._build_auth_headers()
|
|
356
|
+
logger.info("OAuth token refreshed")
|
|
357
|
+
else:
|
|
358
|
+
logger.warning(f"Token refresh failed: {response.status_code}")
|
|
359
|
+
self._session.auth_failures += 1
|
|
360
|
+
else:
|
|
361
|
+
# For other methods, re-authenticate
|
|
362
|
+
await self.initialize()
|
|
363
|
+
|
|
364
|
+
async def validate_session(self, validation_url: str = "", expected_status: int = 200) -> bool:
|
|
365
|
+
"""
|
|
366
|
+
Validate that the authentication session is still valid.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
validation_url: URL to check (should require auth)
|
|
370
|
+
expected_status: Expected HTTP status for valid session
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
True if session is valid
|
|
374
|
+
"""
|
|
375
|
+
url = validation_url or self._session.validation_url
|
|
376
|
+
if not url:
|
|
377
|
+
return True # Can't validate without URL
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
client = await self._get_http_client()
|
|
381
|
+
headers = await self.get_auth_headers()
|
|
382
|
+
|
|
383
|
+
response = await client.get(url, headers=headers)
|
|
384
|
+
|
|
385
|
+
is_valid = response.status_code == expected_status
|
|
386
|
+
self._session.is_valid = is_valid
|
|
387
|
+
|
|
388
|
+
if not is_valid:
|
|
389
|
+
logger.warning(f"Session validation failed: {response.status_code}")
|
|
390
|
+
self._session.auth_failures += 1
|
|
391
|
+
|
|
392
|
+
return is_valid
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.error(f"Session validation error: {e}")
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
async def _get_http_client(self) -> httpx.AsyncClient:
|
|
399
|
+
"""Get HTTP client for auth requests"""
|
|
400
|
+
if self._http_client is None:
|
|
401
|
+
self._http_client = httpx.AsyncClient(
|
|
402
|
+
timeout=30.0,
|
|
403
|
+
follow_redirects=True,
|
|
404
|
+
)
|
|
405
|
+
return self._http_client
|
|
406
|
+
|
|
407
|
+
async def close(self) -> None:
|
|
408
|
+
"""Close HTTP client"""
|
|
409
|
+
if self._http_client:
|
|
410
|
+
await self._http_client.aclose()
|
|
411
|
+
self._http_client = None
|
|
412
|
+
|
|
413
|
+
def get_session_stats(self) -> dict[str, Any]:
|
|
414
|
+
"""Get session statistics"""
|
|
415
|
+
if not self._session:
|
|
416
|
+
return {"status": "not_initialized"}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
"session_id": self._session.session_id,
|
|
420
|
+
"method": self.credentials.method.value,
|
|
421
|
+
"created_at": self._session.created_at.isoformat(),
|
|
422
|
+
"last_used": self._session.last_used.isoformat(),
|
|
423
|
+
"is_valid": self._session.is_valid,
|
|
424
|
+
"requests_made": self._session.requests_made,
|
|
425
|
+
"auth_failures": self._session.auth_failures,
|
|
426
|
+
"token_expires_at": (
|
|
427
|
+
self.credentials.token_expires_at.isoformat()
|
|
428
|
+
if self.credentials.token_expires_at
|
|
429
|
+
else None
|
|
430
|
+
),
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class AuthenticationError(Exception):
|
|
435
|
+
"""Authentication failed"""
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ============================================================================
|
|
440
|
+
# Convenience Functions
|
|
441
|
+
# ============================================================================
|
|
442
|
+
|
|
443
|
+
def create_bearer_auth(token: str, prefix: str = "Bearer") -> AuthCredentials:
|
|
444
|
+
"""Create bearer token authentication"""
|
|
445
|
+
return AuthCredentials(
|
|
446
|
+
method=AuthMethod.BEARER_TOKEN,
|
|
447
|
+
token=token,
|
|
448
|
+
token_prefix=prefix,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def create_basic_auth(username: str, password: str) -> AuthCredentials:
|
|
453
|
+
"""Create basic authentication"""
|
|
454
|
+
return AuthCredentials(
|
|
455
|
+
method=AuthMethod.BASIC_AUTH,
|
|
456
|
+
username=username,
|
|
457
|
+
password=password,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def create_api_key_auth(
|
|
462
|
+
api_key: str,
|
|
463
|
+
header: str = "X-API-Key",
|
|
464
|
+
in_query: bool = False,
|
|
465
|
+
query_param: str = "api_key",
|
|
466
|
+
) -> AuthCredentials:
|
|
467
|
+
"""Create API key authentication"""
|
|
468
|
+
return AuthCredentials(
|
|
469
|
+
method=AuthMethod.API_KEY,
|
|
470
|
+
api_key=api_key,
|
|
471
|
+
api_key_header=header,
|
|
472
|
+
api_key_in_query=in_query,
|
|
473
|
+
api_key_query_param=query_param,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def create_cookie_auth(cookies: dict[str, str]) -> AuthCredentials:
|
|
478
|
+
"""Create cookie-based authentication"""
|
|
479
|
+
return AuthCredentials(
|
|
480
|
+
method=AuthMethod.COOKIE,
|
|
481
|
+
cookies=cookies,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def create_form_login_auth(
|
|
486
|
+
login_url: str,
|
|
487
|
+
username: str,
|
|
488
|
+
password: str,
|
|
489
|
+
username_field: str = "username",
|
|
490
|
+
password_field: str = "password",
|
|
491
|
+
csrf_field: str = "",
|
|
492
|
+
success_indicator: str = "",
|
|
493
|
+
) -> AuthCredentials:
|
|
494
|
+
"""Create form-based login authentication"""
|
|
495
|
+
return AuthCredentials(
|
|
496
|
+
method=AuthMethod.FORM_LOGIN,
|
|
497
|
+
login_url=login_url,
|
|
498
|
+
login_data={
|
|
499
|
+
username_field: username,
|
|
500
|
+
password_field: password,
|
|
501
|
+
},
|
|
502
|
+
csrf_field=csrf_field,
|
|
503
|
+
success_indicator=success_indicator,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def create_oauth2_auth(
|
|
508
|
+
client_id: str,
|
|
509
|
+
client_secret: str,
|
|
510
|
+
token_url: str,
|
|
511
|
+
scope: str = "",
|
|
512
|
+
) -> AuthCredentials:
|
|
513
|
+
"""Create OAuth2 client credentials authentication"""
|
|
514
|
+
return AuthCredentials(
|
|
515
|
+
method=AuthMethod.OAUTH2,
|
|
516
|
+
oauth_client_id=client_id,
|
|
517
|
+
oauth_client_secret=client_secret,
|
|
518
|
+
oauth_token_url=token_url,
|
|
519
|
+
oauth_scope=scope,
|
|
520
|
+
)
|