File size: 2,187 Bytes
e1104b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
"""Small, transport-facing safeguards for social provider data.

Provider access tokens are credentials, not application data.  This module is
intentionally used at every boundary that can serialize provider-controlled
metadata (REST, MCP, SDK, n8n, frontend, and audit records).
"""

from __future__ import annotations

import re
from typing import Any


_SENSITIVE_KEY_PARTS = frozenset(
    {
        "access_token",
        "refresh_token",
        "id_token",
        "token",
        "secret",
        "authorization",
        "cookie",
        "password",
        "api_key",
        "credential",
    }
)
_BEARER = re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]+")
_ASSIGNED_SECRET = re.compile(
    r"(?i)(access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|"
    r"authorization|api[_-]?key|password|secret|credential)"
    r"([\"']?\s*[:=]\s*[\"']?)([^\"'\s,&}]+)"
)


def public_provider_data(value: Any) -> Any:
    """Recursively omit credential-like fields from externally visible data.

    This is defense in depth. TokenService remains the only supported
    credential reader, but provider responses and future metadata additions
    must never be able to bypass that contract accidentally.
    """

    if isinstance(value, dict):
        return {
            str(key): public_provider_data(item)
            for key, item in value.items()
            if not _is_sensitive_key(str(key))
        }
    if isinstance(value, list):
        return [public_provider_data(item) for item in value]
    if isinstance(value, tuple):
        return [public_provider_data(item) for item in value]
    if isinstance(value, str):
        return redact_sensitive_text(value)
    return value


def _is_sensitive_key(key: str) -> bool:
    normalized = key.strip().lower().replace("-", "_")
    return any(part in normalized for part in _SENSITIVE_KEY_PARTS)


def redact_sensitive_text(value: str) -> str:
    """Remove recognizable credentials embedded in provider-controlled text."""

    redacted = _BEARER.sub("Bearer [REDACTED]", value)
    return _ASSIGNED_SECRET.sub(
        lambda match: f"{match.group(1)}{match.group(2)}[REDACTED]",
        redacted,
    )