feat: support upstream token refresh
This commit is contained in:
@@ -9,10 +9,12 @@ from urllib.parse import urlparse
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Keys likely to contain auth tokens in storage
|
||||
TOKEN_KEYS = frozenset({
|
||||
"token", "access_token", "accessToken", "jwt", "auth_token", "authToken",
|
||||
"refresh_token", "refreshToken", "id_token", "session_token",
|
||||
ACCESS_TOKEN_KEYS = frozenset({
|
||||
"token", "access_token", "accesstoken", "jwt", "auth_token", "authtoken",
|
||||
"id_token", "idtoken", "session_token", "sessiontoken",
|
||||
})
|
||||
REFRESH_TOKEN_KEYS = frozenset({"refresh_token", "refreshtoken"})
|
||||
TOKEN_KEYS = ACCESS_TOKEN_KEYS | REFRESH_TOKEN_KEYS
|
||||
SECRET_KEYS = frozenset({
|
||||
"secret", "api_key", "apiKey", "apikey",
|
||||
})
|
||||
@@ -116,6 +118,27 @@ def _curate_candidates(
|
||||
extra=bundle_extra,
|
||||
)
|
||||
|
||||
# 0.5. Sub2API browser sessions keep access token and refresh token separately.
|
||||
storage_sources = [("localStorage", local_storage), ("sessionStorage", session_storage)]
|
||||
access_entry = _find_storage_token_entry(storage_sources, ACCESS_TOKEN_KEYS)
|
||||
refresh_entry = _find_storage_token_entry(storage_sources, REFRESH_TOKEN_KEYS)
|
||||
if access_entry and refresh_entry:
|
||||
access_source, access_value = access_entry
|
||||
refresh_source, refresh_value = refresh_entry
|
||||
_add(
|
||||
candidates,
|
||||
"bearer_token",
|
||||
f"{access_source}+{refresh_source}",
|
||||
access_value,
|
||||
_preview(access_value),
|
||||
"Sub2API Bearer + Refresh Token",
|
||||
96,
|
||||
extra={
|
||||
"refresh_token": refresh_value,
|
||||
"refresh_token_preview": _preview(refresh_value),
|
||||
},
|
||||
)
|
||||
|
||||
# 1. CDP-captured network headers (high confidence)
|
||||
seen = set()
|
||||
for h in auth_headers:
|
||||
@@ -141,9 +164,10 @@ def _curate_candidates(
|
||||
if not isinstance(val, str) or not val:
|
||||
continue
|
||||
key_lower = key.lower()
|
||||
is_refresh_key = any(k in key_lower for k in REFRESH_TOKEN_KEYS)
|
||||
|
||||
# Explicit auth-named keys
|
||||
if any(k in key_lower for k in TOKEN_KEYS):
|
||||
if any(k in key_lower for k in TOKEN_KEYS) and not is_refresh_key:
|
||||
preview = _preview(val)
|
||||
score = 85 if "token" in key_lower and val.count(".") >= 2 else 75
|
||||
_add(candidates, "bearer_token", f"{store_name}.{key}", val, preview,
|
||||
@@ -153,14 +177,14 @@ def _curate_candidates(
|
||||
f"{store_name}.{key}", 70)
|
||||
|
||||
# Looks like a JWT (xx.yy.zz format)
|
||||
if val.count(".") >= 2 and 20 < len(val) < 5000:
|
||||
if not is_refresh_key and val.count(".") >= 2 and 20 < len(val) < 5000:
|
||||
if val not in seen:
|
||||
seen.add(val)
|
||||
_add(candidates, "bearer_token", f"{store_name}.{key}", val, _preview(val),
|
||||
f"{store_name}.{key} (JWT)", 80)
|
||||
|
||||
# sk-xxx API key pattern
|
||||
if val.startswith("sk-") and len(val) > 10:
|
||||
if not is_refresh_key and val.startswith("sk-") and len(val) > 10:
|
||||
_add(candidates, "bearer_token", f"{store_name}.{key}", val, _preview(val),
|
||||
f"{store_name}.{key} (API Key)", 90)
|
||||
|
||||
@@ -198,6 +222,19 @@ def _find_user_header(headers: list[dict[str, str]]) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _find_storage_token_entry(
|
||||
stores: list[tuple[str, dict[str, str]]],
|
||||
key_names: frozenset[str],
|
||||
) -> tuple[str, str] | None:
|
||||
for store_name, store in stores:
|
||||
for key, value in store.items():
|
||||
if key.lower() not in key_names:
|
||||
continue
|
||||
if isinstance(value, str) and value.strip():
|
||||
return f"{store_name}.{key}", value.strip()
|
||||
return None
|
||||
|
||||
|
||||
def _find_storage_value(*stores: dict[str, str], key: str) -> str:
|
||||
for store in stores:
|
||||
value = store.get(key)
|
||||
|
||||
Reference in New Issue
Block a user