Return a non-expired access token, refreshing if needed.
(self)
| 1022 | self._http.close() |
| 1023 | |
| 1024 | def current_access_token(self) -> str: |
| 1025 | """Return a non-expired access token, refreshing if needed.""" |
| 1026 | with self._lock: |
| 1027 | if self._bundle is None: |
| 1028 | self._bundle = _read_oidc_token_bundle(self._gateway_dir) |
| 1029 | if self._bundle is None: |
| 1030 | raise SandboxError( |
| 1031 | f"OIDC token for gateway '{self._cluster_name}' is " |
| 1032 | f"missing or unreadable. Re-authenticate with: " |
| 1033 | f"openshell gateway login" |
| 1034 | ) |
| 1035 | if self._is_fresh(self._bundle): |
| 1036 | return self._bundle["access_token"] |
| 1037 | # Cached bundle is stale. Before refreshing, re-read disk — |
| 1038 | # another process (CLI, TUI, another SDK client) may have |
| 1039 | # rotated the bundle while we were idle. Adopt the disk |
| 1040 | # bundle when it was refreshed more recently than ours, EVEN |
| 1041 | # WHEN its access token is also stale: otherwise we'd refresh |
| 1042 | # with our in-memory refresh_token, which a rotating IdP may |
| 1043 | # have already invalidated when the other process refreshed |
| 1044 | # (Keycloak with rotation, Entra in strict mode). |
| 1045 | # |
| 1046 | # "More recently" is judged by `expires_at`: a refresh issues |
| 1047 | # a new access token with a forward expiry alongside the |
| 1048 | # (possibly rotated) refresh_token, so the bundle with the |
| 1049 | # later expiry carries the newest refresh_token. This also |
| 1050 | # preserves the write_back=False case, where our in-memory |
| 1051 | # bundle has already rotated past the on-disk one and must |
| 1052 | # NOT be clobbered by the older disk copy. |
| 1053 | disk = _read_oidc_token_bundle(self._gateway_dir) |
| 1054 | if disk is not None and self._expiry(disk) > self._expiry(self._bundle): |
| 1055 | # If the issuer changed under us, the cached token |
| 1056 | # endpoint no longer applies — force re-discovery. |
| 1057 | if _normalize_issuer(disk) != _normalize_issuer(self._bundle): |
| 1058 | self._token_endpoint = None |
| 1059 | self._bundle = disk |
| 1060 | if self._is_fresh(disk): |
| 1061 | return disk["access_token"] |
| 1062 | # Truly stale; refresh against the IdP using the freshest |
| 1063 | # bundle we have (disk if it was newer, else in-memory). |
| 1064 | try: |
| 1065 | self._bundle = self._refresh(self._bundle) |
| 1066 | except _InvalidGrantError as exc: |
| 1067 | # We lost a cross-process rotation race: between our disk |
| 1068 | # re-read above and our refresh POST, a peer (CLI, TUI, |
| 1069 | # another SDK client) rotated the refresh_token and the |
| 1070 | # IdP invalidated ours. This is the residual window that |
| 1071 | # neither google-auth nor botocore close without an OS |
| 1072 | # file lock. Rather than lock, recover: re-read disk once |
| 1073 | # and, if a peer wrote a *different* refresh_token, retry |
| 1074 | # with it before surfacing a re-authenticate error. |
| 1075 | self._bundle = self._recover_from_invalid_grant(self._bundle, exc) |
| 1076 | if self._write_back: |
| 1077 | self._write_to_disk(self._bundle) |
| 1078 | return self._bundle["access_token"] |
| 1079 | |
| 1080 | def _recover_from_invalid_grant( |
| 1081 | self, attempted: dict, exc: _InvalidGrantError |