Completes the authorize step in-process by following the redirect through the bridge. `redirect_handler` GETs the authorize URL on the bound client (with `auth=None` so the request does not re-enter the locked auth flow), parses `code` and `state` from the 302 `Location`, and stashes th
| 127 | |
| 128 | |
| 129 | class HeadlessOAuth: |
| 130 | """Completes the authorize step in-process by following the redirect through the bridge. |
| 131 | |
| 132 | `redirect_handler` GETs the authorize URL on the bound client (with `auth=None` so the |
| 133 | request does not re-enter the locked auth flow), parses `code` and `state` from the 302 |
| 134 | `Location`, and stashes them; `callback_handler` returns the stashed pair. Tests inspect |
| 135 | `authorize_url` to assert what the SDK put on the authorize request. |
| 136 | |
| 137 | `state_override`: when set, `callback_handler` returns this value as the state instead of |
| 138 | the one parsed from the redirect, so tests can drive the state-mismatch path. |
| 139 | |
| 140 | `iss_override`: when set, `callback_handler` returns this value as the RFC 9207 issuer |
| 141 | instead of the one parsed from the redirect, so tests can drive the iss-mismatch path. |
| 142 | """ |
| 143 | |
| 144 | def __init__(self, *, state_override: str | None = None, iss_override: str | None = None) -> None: |
| 145 | self.authorize_url: str | None = None |
| 146 | self.authorize_urls: list[str] = [] |
| 147 | self.error: str | None = None |
| 148 | self._state_override = state_override |
| 149 | self._iss_override = iss_override |
| 150 | self._http: httpx.AsyncClient | None = None |
| 151 | self._code: str = "" |
| 152 | self._state: str | None = None |
| 153 | self._iss: str | None = None |
| 154 | |
| 155 | def bind(self, http_client: httpx.AsyncClient) -> None: |
| 156 | self._http = http_client |
| 157 | |
| 158 | async def redirect_handler(self, authorization_url: str) -> None: |
| 159 | assert self._http is not None |
| 160 | self.authorize_url = authorization_url |
| 161 | self.authorize_urls.append(authorization_url) |
| 162 | # auth=None is load-bearing: without it the GET re-enters OAuthClientProvider.async_auth_flow |
| 163 | # through its context lock and the flow deadlocks. |
| 164 | response = await self._http.get(authorization_url, follow_redirects=False, auth=None) |
| 165 | assert response.status_code == 302, f"authorize endpoint returned {response.status_code}: {response.text}" |
| 166 | params = parse_qs(urlsplit(response.headers["location"]).query) |
| 167 | self._code = params.get("code", [""])[0] |
| 168 | self._state = params.get("state", [None])[0] |
| 169 | self._iss = params.get("iss", [None])[0] |
| 170 | self.error = params.get("error", [None])[0] |
| 171 | |
| 172 | async def callback_handler(self) -> AuthorizationCodeResult: |
| 173 | return AuthorizationCodeResult( |
| 174 | code=self._code, |
| 175 | state=self._state_override if self._state_override is not None else self._state, |
| 176 | iss=self._iss_override if self._iss_override is not None else self._iss, |
| 177 | ) |
| 178 | |
| 179 | |
| 180 | def auth_settings( |
no outgoing calls