Skip to content

Commit 5181a9d

Browse files
feat: Introduce token redaction utility and apply it to log messages for enhanced security.
1 parent 5ef98c0 commit 5181a9d

File tree

5 files changed

+27
-14
lines changed

5 files changed

+27
-14
lines changed

app/api/endpoints/catalogs.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fastapi import APIRouter, HTTPException, Response
44
from loguru import logger
55

6+
from app.core.security import redact_token
67
from app.core.settings import UserSettings, get_default_settings
78
from app.services.catalog_updater import refresh_catalogs_for_credentials
89
from app.services.recommendation_service import RecommendationService
@@ -40,7 +41,7 @@ async def get_catalog(type: str, id: str, response: Response, token: str):
4041
),
4142
)
4243

43-
logger.info(f"[{token}] Fetching catalog for {type} with id {id}")
44+
logger.info(f"[{redact_token(token)}] Fetching catalog for {type} with id {id}")
4445

4546
credentials = await token_store.get_user_data(token)
4647
if not credentials:
@@ -88,7 +89,7 @@ async def get_catalog(type: str, id: str, response: Response, token: str):
8889
except HTTPException:
8990
raise
9091
except Exception as e:
91-
logger.exception(f"[{token}] Error fetching catalog for {type}/{id}: {e}")
92+
logger.exception(f"[{redact_token(token)}] Error fetching catalog for {type}/{id}: {e}")
9293
raise HTTPException(status_code=500, detail=str(e))
9394

9495

@@ -100,7 +101,7 @@ async def update_catalogs(token: str):
100101
# Decode credentials from path
101102
credentials = await token_store.get_user_data(token)
102103

103-
logger.info(f"[{token}] Updating catalogs in response to manual request")
104+
logger.info(f"[{redact_token(token)}] Updating catalogs in response to manual request")
104105
updated = await refresh_catalogs_for_credentials(token, credentials)
105106
logger.info(f"Manual catalog update completed: {updated}")
106107
return {"success": updated}

app/api/endpoints/tokens.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from redis import exceptions as redis_exceptions
66

77
from app.core.config import settings
8+
from app.core.security import redact_token
89
from app.core.settings import CatalogConfig, UserSettings, get_default_settings
910
from app.services.stremio_service import StremioService
1011
from app.services.token_store import token_store
@@ -93,8 +94,6 @@ async def create_token(payload: TokenRequest, request: Request) -> TokenResponse
9394
# 2. Check if user already exists
9495
token = token_store.get_token_from_user_id(user_id)
9596
existing_data = await token_store.get_user_data(token)
96-
if not existing_data:
97-
raise HTTPException(status_code=401, detail="Invalid or expired token. Please reconfigure the addon.")
9897

9998
# 3. Construct Settings
10099
default_settings = get_default_settings()
@@ -122,7 +121,7 @@ async def create_token(payload: TokenRequest, request: Request) -> TokenResponse
122121
# 6. Store user data
123122
try:
124123
token = await token_store.store_user_data(user_id, payload_to_store)
125-
logger.info(f"[{token}] Account {'created' if is_new_account else 'updated'} for user {user_id}")
124+
logger.info(f"[{redact_token(token)}] Account {'created' if is_new_account else 'updated'} for user {user_id}")
126125
except RuntimeError as exc:
127126
raise HTTPException(status_code=500, detail="Server configuration error.") from exc
128127
except (redis_exceptions.RedisError, OSError) as exc:
@@ -199,7 +198,7 @@ async def delete_token(payload: TokenRequest):
199198

200199
# Delete the token
201200
await token_store.delete_token(token)
202-
logger.info(f"[{token}] Token deleted for user {user_id}")
201+
logger.info(f"[{redact_token(token)}] Token deleted for user {user_id}")
203202
return {"detail": "Settings deleted successfully"}
204203
except HTTPException:
205204
raise

app/core/security.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
def redact_token(token: str | None) -> str:
2+
"""
3+
Redact a token for logging purposes.
4+
Shows the first 6 characters followed by ***.
5+
"""
6+
if not token:
7+
return "None"
8+
if len(token) <= 6:
9+
return token
10+
return f"{token[:6]}***"

app/services/catalog_updater.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from loguru import logger
99

1010
from app.core.config import settings
11+
from app.core.security import redact_token
1112
from app.core.settings import UserSettings, get_default_settings
1213
from app.services.catalog import DynamicCatalogService
1314
from app.services.stremio_service import StremioService
@@ -19,7 +20,7 @@
1920

2021
async def refresh_catalogs_for_credentials(token: str, credentials: dict[str, Any]) -> bool:
2122
if not credentials:
22-
logger.warning(f"[{token}] Attempted to refresh catalogs with no credentials.")
23+
logger.warning(f"[{redact_token(token)}] Attempted to refresh catalogs with no credentials.")
2324
raise HTTPException(status_code=401, detail="Invalid or expired token. Please reconfigure the addon.")
2425

2526
auth_key = credentials.get("authKey")
@@ -49,7 +50,7 @@ async def refresh_catalogs_for_credentials(token: str, credentials: dict[str, An
4950
catalogs = await dynamic_catalog_service.get_dynamic_catalogs(
5051
library_items=library_items, user_settings=user_settings
5152
)
52-
logger.info(f"[{token}] Prepared {len(catalogs)} catalogs")
53+
logger.info(f"[{redact_token(token)}] Prepared {len(catalogs)} catalogs")
5354
return await stremio_service.update_catalogs(catalogs, auth_key)
5455
except Exception as e:
5556
logger.exception(f"Failed to update catalogs: {e}", exc_info=True)
@@ -118,18 +119,18 @@ async def refresh_all_tokens(self) -> None:
118119
async def _update_safe(key: str, payload: dict[str, Any]) -> None:
119120
if not payload.get("authKey"):
120121
logger.debug(
121-
f"Skipping token {key} with incomplete credentials",
122+
f"Skipping token {redact_token(key)} with incomplete credentials",
122123
)
123124
return
124125

125126
async with sem:
126127
try:
127128
updated = await refresh_catalogs_for_credentials(key, payload)
128129
logger.info(
129-
f"Background refresh for {key} completed (updated={updated})",
130+
f"Background refresh for {redact_token(key)} completed (updated={updated})",
130131
)
131132
except Exception as exc:
132-
logger.error(f"Background refresh failed for {key}: {exc}", exc_info=True)
133+
logger.error(f"Background refresh failed for {redact_token(key)}: {exc}", exc_info=True)
133134

134135
try:
135136
async for key, payload in token_store.iter_payloads():

app/services/token_store.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from loguru import logger
1212

1313
from app.core.config import settings
14+
from app.core.security import redact_token
1415

1516

1617
class TokenStore:
@@ -20,6 +21,7 @@ class TokenStore:
2021

2122
def __init__(self) -> None:
2223
self._client: redis.Redis | None = None
24+
self._cipher: Fernet | None = None
2325
# Cache decrypted payloads for 1 day (86400s) to reduce Redis hits
2426
# Max size 5000 allows many active users without eviction
2527
self._payload_cache: TTLCache = TTLCache(maxsize=5000, ttl=86400)
@@ -148,7 +150,7 @@ async def iter_payloads(self) -> AsyncIterator[tuple[str, dict[str, Any]]]:
148150
try:
149151
data_raw = await client.get(key)
150152
except (redis.RedisError, OSError) as exc:
151-
logger.warning(f"Failed to fetch payload for {key}: {exc}")
153+
logger.warning(f"Failed to fetch payload for {redact_token(key)}: {exc}")
152154
continue
153155

154156
if not data_raw:
@@ -157,7 +159,7 @@ async def iter_payloads(self) -> AsyncIterator[tuple[str, dict[str, Any]]]:
157159
try:
158160
payload = json.loads(data_raw)
159161
except json.JSONDecodeError:
160-
logger.warning(f"Failed to decode payload for key {key}. Skipping.")
162+
logger.warning(f"Failed to decode payload for key {redact_token(key)}. Skipping.")
161163
continue
162164

163165
yield key, payload

0 commit comments

Comments
 (0)