Thread-safe in-process OAuth2 refresh for a gateway's `oidc_token.json`. Mirrors the lazy-refresh pattern used by `google-auth`'s `Credentials` and `botocore`'s `SSOTokenProvider`. Uses `httpx` for transport so we can pin `follow_redirects=False` and the TLS verification policy expl
| 930 | |
| 931 | |
| 932 | class _OidcRefresher: |
| 933 | """Thread-safe in-process OAuth2 refresh for a gateway's `oidc_token.json`. |
| 934 | |
| 935 | Mirrors the lazy-refresh pattern used by `google-auth`'s `Credentials` |
| 936 | and `botocore`'s `SSOTokenProvider`. Uses `httpx` for transport so |
| 937 | we can pin `follow_redirects=False` and the TLS verification policy |
| 938 | explicitly — same posture as the Rust CLI's use of `reqwest` with |
| 939 | `Policy::none()` and an opt-in `danger_accept_invalid_certs` (see |
| 940 | `crates/openshell-cli/src/oidc_auth.rs::http_client`). The OAuth2 |
| 941 | refresh-token grant itself (RFC 6749 §6) is a single form-encoded |
| 942 | POST, handled inline rather than via an OAuth2 library. |
| 943 | |
| 944 | Properties: |
| 945 | |
| 946 | - **Lazy**: check expiry on every RPC; refresh only when stale. |
| 947 | - **Lock-coordinated**: concurrent RPCs share a single refresh, not |
| 948 | one per call. Plain `threading.Lock` (no separate worker thread, |
| 949 | unlike `google-auth`'s `RefreshThreadManager` — sufficient for |
| 950 | our use case). |
| 951 | - **Disk-aware**: before refreshing, re-read `oidc_token.json` — |
| 952 | the CLI or another process may have already rotated the bundle. |
| 953 | - **Discovery-validated**: fetches the OIDC discovery document |
| 954 | from `<issuer>/.well-known/openid-configuration`, rejects |
| 955 | responses whose `issuer` field doesn't match the configured one |
| 956 | (preventing SSRF / misdirection of the refresh_token to an |
| 957 | attacker-controlled endpoint). Mirrors the Rust CLI's |
| 958 | `discover()` function. |
| 959 | - **Redirect-hardened**: `follow_redirects=False` on the underlying |
| 960 | `httpx.Client` so a 3xx during discovery or refresh is treated |
| 961 | as a failure rather than silently chasing the redirect to an |
| 962 | arbitrary host. |
| 963 | - **Write-back by default**: when `auto_refresh=True`, refreshed |
| 964 | bundles are atomically persisted to `oidc_token.json` at mode |
| 965 | 0600 so other processes (Rust CLI, TUI, other Python clients) |
| 966 | see the rotated `refresh_token`. Required for IdPs that |
| 967 | invalidate the old `refresh_token` on rotation (Keycloak with |
| 968 | rotation enabled, Entra in strict mode); without write-back, a |
| 969 | second process would `invalid_grant` on next refresh. |
| 970 | - **`insecure=True` flag** disables TLS certificate verification |
| 971 | for the discovery and refresh calls. Matches the Rust CLI's |
| 972 | `--insecure` plumbing for OIDC issuers behind self-signed certs. |
| 973 | - **Refresh failures** surface as `SandboxError` with a |
| 974 | "re-authenticate with: openshell gateway login" hint. |
| 975 | """ |
| 976 | |
| 977 | def __init__( |
| 978 | self, |
| 979 | gateway_dir: pathlib.Path, |
| 980 | cluster_name: str, |
| 981 | *, |
| 982 | write_back: bool = True, |
| 983 | insecure: bool = False, |
| 984 | ) -> None: |
| 985 | self._gateway_dir = gateway_dir |
| 986 | self._cluster_name = cluster_name |
| 987 | self._write_back = write_back |
| 988 | self._lock = threading.Lock() |
| 989 | self._bundle: dict | None = None |
no outgoing calls