diff --git a/githubkit/auth/app.py b/githubkit/auth/app.py index acd70e7719..286d6d678e 100644 --- a/githubkit/auth/app.py +++ b/githubkit/auth/app.py @@ -11,7 +11,7 @@ from githubkit.utils import UNSET, Unset, exclude_unset from ._url import require_app_auth, require_basic_auth, require_bypass -from .base import BaseAuthStrategy +from .base import BaseAuthStrategy, build_request_with_options from .oauth import OAuthAppAuthStrategy try: @@ -104,7 +104,7 @@ async def aget_jwt(self) -> str: await cache.aset(cache_key, token, timedelta(minutes=8)) return token - def _build_installation_auth_request(self) -> httpx.Request: + def _build_installation_auth_request(self, request: httpx.Request) -> httpx.Request: from githubkit_schemas.latest.models import ( AppInstallationsInstallationIdAccessTokensPostBody, ) @@ -130,9 +130,11 @@ def _build_installation_auth_request(self) -> httpx.Request: }, ) ) - return httpx.Request( + return build_request_with_options( + self.github, "POST", url, + request=request, json=exclude_unset(body), headers={ "User-Agent": self.github.config.user_agent, @@ -207,7 +209,7 @@ def sync_auth_flow( cache = self.github.config.cache_strategy.get_cache_storage() key = self._get_installation_cache_key() if not (token := cache.get(key)): - token_request = self._build_installation_auth_request() + token_request = self._build_installation_auth_request(request) token_request.headers["Authorization"] = f"Bearer {self.get_jwt()}" response = yield token_request response.read() @@ -249,7 +251,7 @@ async def async_auth_flow( cache = await self.github.config.cache_strategy.get_async_cache_storage() key = self._get_installation_cache_key() if not (token := await cache.aget(key)): - token_request = self._build_installation_auth_request() + token_request = self._build_installation_auth_request(request) token_request.headers["Authorization"] = f"Bearer {await self.aget_jwt()}" response = yield token_request await response.aread() diff --git a/githubkit/auth/base.py b/githubkit/auth/base.py index 953fd6bd5d..3f0e724f18 100644 --- a/githubkit/auth/base.py +++ b/githubkit/auth/base.py @@ -1,5 +1,5 @@ import abc -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import httpx @@ -11,3 +11,23 @@ class BaseAuthStrategy(abc.ABC): @abc.abstractmethod def get_auth_flow(self, github: "GitHubCore") -> httpx.Auth: raise NotImplementedError + + +def build_request_with_options( + github: "GitHubCore", + method: str, + url: httpx.URL, + *, + request: httpx.Request | None = None, + **kwargs: Any, +) -> httpx.Request: + """Build a request used inside auth flow, keeping the client timeout. + + httpx only sets the timeout extension on the request passed to `send`, + not on requests yielded by an auth flow, so copy it from the original + request, or fall back to the client config. + """ + timeout = ( + request.extensions.get("timeout") if request is not None else None + ) or github.config.timeout.as_dict() + return httpx.Request(method, url, extensions={"timeout": timeout}, **kwargs) diff --git a/githubkit/auth/oauth.py b/githubkit/auth/oauth.py index 3c662eacd1..afe08775eb 100644 --- a/githubkit/auth/oauth.py +++ b/githubkit/auth/oauth.py @@ -11,7 +11,7 @@ from githubkit.utils import is_async from ._url import get_oauth_base_url, require_basic_auth, require_bypass -from .base import BaseAuthStrategy +from .base import BaseAuthStrategy, build_request_with_options try: import anyio @@ -29,7 +29,10 @@ def create_device_code( - github: "GitHubCore", client_id: str, scopes: Sequence[str] | None = None + github: "GitHubCore", + client_id: str, + scopes: Sequence[str] | None = None, + request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Create a device code for OAuth.""" base_url = get_oauth_base_url(github.config.base_url) @@ -37,9 +40,11 @@ def create_device_code( body = {"client_id": client_id} if scopes: body["scope"] = " ".join(scopes) - response = yield httpx.Request( + response = yield build_request_with_options( + github, "POST", url, + request=request, json=body, headers={ "User-Agent": github.config.user_agent, @@ -56,6 +61,7 @@ def exchange_web_flow_code( client_secret: str, code: str, redirect_uri: str | None = None, + request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Exchange web flow code for token.""" base_url = get_oauth_base_url(github.config.base_url) @@ -67,9 +73,11 @@ def exchange_web_flow_code( } if redirect_uri: body["redirect_uri"] = redirect_uri - response = yield httpx.Request( + response = yield build_request_with_options( + github, "POST", url, + request=request, json=body, headers={ "User-Agent": github.config.user_agent, @@ -81,14 +89,19 @@ def exchange_web_flow_code( def exchange_device_code( - github: "GitHubCore", client_id: str, device_code: str + github: "GitHubCore", + client_id: str, + device_code: str, + request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Exchange device code for token.""" base_url = get_oauth_base_url(github.config.base_url) url = base_url.copy_with(raw_path=base_url.raw_path + b"login/oauth/access_token") - response = yield httpx.Request( + response = yield build_request_with_options( + github, "POST", url, + request=request, json={ "client_id": client_id, "grant_type": "urn:ietf:params:oauth:grant-type:device_code", @@ -108,13 +121,16 @@ def refresh_token( client_id: str, client_secret: str | None, # client secret is optional in device flow refresh_token: str, + request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Refresh token.""" base_url = get_oauth_base_url(github.config.base_url) url = base_url.copy_with(raw_path=base_url.raw_path + b"login/oauth/access_token") - response = yield httpx.Request( + response = yield build_request_with_options( + github, "POST", url, + request=request, json={ "client_id": client_id, "client_secret": client_secret, @@ -287,6 +303,7 @@ def auth_flow( self.client_id, self.client_secret, cast(str, self.refresh_token), + request, ) result = _parse_token_exchange_response(data) self.token = token = result["token"] @@ -343,6 +360,7 @@ def sync_auth_flow( self.client_secret, self.code, self.redirect_uri, + request, ) exchange_request = next(flow) while True: @@ -374,6 +392,7 @@ async def async_auth_flow( self.client_secret, self.code, self.redirect_uri, + request, ) exchange_request = next(flow) while True: @@ -442,7 +461,7 @@ def sync_auth_flow( # exchange token for the first time if (auth_strategy := self._token_auth_strategy) is None: # create device code - flow = create_device_code(self.github, self.client_id, self.scopes) + flow = create_device_code(self.github, self.client_id, self.scopes, request) create_request = next(flow) while True: response = yield create_request @@ -462,7 +481,7 @@ def sync_auth_flow( if datetime.now(timezone.utc) > result["expire_time"]: raise AuthExpiredError("Device code expired.") flow = exchange_device_code( - self.github, self.client_id, result["device_code"] + self.github, self.client_id, result["device_code"], request ) auth_request = next(flow) while True: @@ -503,7 +522,7 @@ async def async_auth_flow( # exchange token for the first time if (auth_strategy := self._token_auth_strategy) is None: # create device code - flow = create_device_code(self.github, self.client_id, self.scopes) + flow = create_device_code(self.github, self.client_id, self.scopes, request) create_request = next(flow) while True: response = yield create_request @@ -523,7 +542,7 @@ async def async_auth_flow( if datetime.now(timezone.utc) > result["expire_time"]: raise AuthExpiredError("Device code expired.") flow = exchange_device_code( - self.github, self.client_id, result["device_code"] + self.github, self.client_id, result["device_code"], request ) auth_request = next(flow) while True: