MCPcopy Index your code
hub / github.com/modelcontextprotocol/python-sdk / HeadlessOAuth

Class HeadlessOAuth

tests/interaction/auth/_harness.py:129–177  ·  view source on GitHub ↗

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

Source from the content-addressed store, hash-verified

127
128
129class 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
180def auth_settings(

Calls

no outgoing calls