From 5155d921b38fc8724ed9f0929fc74c49a4b95b82 Mon Sep 17 00:00:00 2001 From: Matt Black Date: Mon, 8 Jul 2024 11:45:54 +1000 Subject: [PATCH 1/5] feat: Raise meaningful exception when oauth callback times out --- google_auth_oauthlib/flow.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/google_auth_oauthlib/flow.py b/google_auth_oauthlib/flow.py index e564ca4..e12da98 100644 --- a/google_auth_oauthlib/flow.py +++ b/google_auth_oauthlib/flow.py @@ -410,8 +410,9 @@ def run_local_server( in the user's browser. redirect_uri_trailing_slash (bool): whether or not to add trailing slash when constructing the redirect_uri. Default value is True. - timeout_seconds (int): It will raise an error after the timeout timing - if there are no credentials response. The value is in seconds. + timeout_seconds (int): It will raise a WSGITimeout exception after the + timeout timing if there are no credentials response. The value is in + seconds. When set to None there is no timeout. Default value is None. token_audience (str): Passed along with the request for an access @@ -425,6 +426,10 @@ def run_local_server( Returns: google.oauth2.credentials.Credentials: The OAuth 2.0 credentials for the user. + + Raises: + WSGITimeout: If there is a timeout when waiting for the response from the + authorization server. """ wsgi_app = _RedirectWSGIApp(success_message) # Fail fast if the address is occupied @@ -452,6 +457,10 @@ def run_local_server( local_server.timeout = timeout_seconds local_server.handle_request() + if wsgi_app.last_request_uri is None: + # Timeout occurred + raise WSGITimeout("Timed out waiting for response from authorization server") + # Note: using https here because oauthlib is very picky that # OAuth 2.0 should only occur over https. authorization_response = wsgi_app.last_request_uri.replace("http", "https") @@ -505,3 +514,7 @@ def __call__(self, environ, start_response): start_response("200 OK", [("Content-type", "text/plain; charset=utf-8")]) self.last_request_uri = wsgiref.util.request_uri(environ) return [self._success_message.encode("utf-8")] + + +class WSGITimeout(Exception): + """Raised when the WSGI server times out waiting for a response.""" From e87abfb842c387a3b5a464f5dfe3e450244e3309 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 17 Feb 2026 08:25:29 -0500 Subject: [PATCH 2/5] adds unit test, alters error handling code --- google_auth_oauthlib/flow.py | 18 +++++++++++++----- tests/unit/test_flow.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/google_auth_oauthlib/flow.py b/google_auth_oauthlib/flow.py index ee3dc2f..7bfdfbd 100644 --- a/google_auth_oauthlib/flow.py +++ b/google_auth_oauthlib/flow.py @@ -458,13 +458,21 @@ def run_local_server( local_server.timeout = timeout_seconds local_server.handle_request() - if wsgi_app.last_request_uri is None: - # Timeout occurred - raise WSGITimeout("Timed out waiting for response from authorization server") + # if wsgi_app.last_request_uri is None: + # # Timeout occurred + # raise WSGITimeout("Timed out waiting for response from authorization server") # Note: using https here because oauthlib is very picky that # OAuth 2.0 should only occur over https. - authorization_response = wsgi_app.last_request_uri.replace("http", "https") + try: + authorization_response = wsgi_app.last_request_uri.replace( + "http", "https" + ) + except AttributeError as e: + raise WSGITimeout( + "Timed out waiting for response from authorization server" + ) from e + self.fetch_token( authorization_response=authorization_response, audience=token_audience ) @@ -517,5 +525,5 @@ def __call__(self, environ, start_response): return [self._success_message.encode("utf-8")] -class WSGITimeout(Exception): +class WSGITimeout(AttributeError): """Raised when the WSGI server times out waiting for a response.""" diff --git a/tests/unit/test_flow.py b/tests/unit/test_flow.py index d9833fe..0806460 100644 --- a/tests/unit/test_flow.py +++ b/tests/unit/test_flow.py @@ -496,3 +496,20 @@ def test_run_local_server_logs_and_prints_url( urllib.parse.quote(instance.redirect_uri, safe="") in print_mock.call_args[0][0] ) + + @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) + @mock.patch("wsgiref.simple_server.make_server", autospec=True) + def test_run_local_server_timeout( + self, make_server_mock, webbrowser_mock, instance, mock_fetch_token + ): + mock_server = mock.Mock() + make_server_mock.return_value = mock_server + + # handle_request does nothing (simulating timeout), so last_request_uri remains None + mock_server.handle_request.return_value = None + + with pytest.raises(flow.WSGITimeout): + instance.run_local_server(timeout_seconds=1) + + webbrowser_mock.get.assert_called_with(None) + webbrowser_mock.get.return_value.open.assert_called_once() From b54d8c2393a851c3b1b3dc0af236c0d261616c4b Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Tue, 17 Feb 2026 08:29:33 -0500 Subject: [PATCH 3/5] Apply suggestion from @chalmerlowe Removing temporarily commented out code. --- google_auth_oauthlib/flow.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/google_auth_oauthlib/flow.py b/google_auth_oauthlib/flow.py index 7bfdfbd..ad8d390 100644 --- a/google_auth_oauthlib/flow.py +++ b/google_auth_oauthlib/flow.py @@ -458,10 +458,6 @@ def run_local_server( local_server.timeout = timeout_seconds local_server.handle_request() - # if wsgi_app.last_request_uri is None: - # # Timeout occurred - # raise WSGITimeout("Timed out waiting for response from authorization server") - # Note: using https here because oauthlib is very picky that # OAuth 2.0 should only occur over https. try: From 2055e254be752359a88ffb44a74aede1686979a7 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 17 Feb 2026 08:44:18 -0500 Subject: [PATCH 4/5] updates Error name --- google_auth_oauthlib/flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/google_auth_oauthlib/flow.py b/google_auth_oauthlib/flow.py index ad8d390..bc014a6 100644 --- a/google_auth_oauthlib/flow.py +++ b/google_auth_oauthlib/flow.py @@ -410,7 +410,7 @@ def run_local_server( in the user's browser. redirect_uri_trailing_slash (bool): whether or not to add trailing slash when constructing the redirect_uri. Default value is True. - timeout_seconds (int): It will raise a WSGITimeout exception after the + timeout_seconds (int): It will raise a WSGITimeoutError exception after the timeout timing if there are no credentials response. The value is in seconds. When set to None there is no timeout. @@ -428,7 +428,7 @@ def run_local_server( for the user. Raises: - WSGITimeout: If there is a timeout when waiting for the response from the + WSGITimeoutError: If there is a timeout when waiting for the response from the authorization server. """ wsgi_app = _RedirectWSGIApp(success_message) @@ -465,7 +465,7 @@ def run_local_server( "http", "https" ) except AttributeError as e: - raise WSGITimeout( + raise WSGITimeoutError( "Timed out waiting for response from authorization server" ) from e @@ -521,5 +521,5 @@ def __call__(self, environ, start_response): return [self._success_message.encode("utf-8")] -class WSGITimeout(AttributeError): +class WSGITimeoutError(AttributeError): """Raised when the WSGI server times out waiting for a response.""" From 3972e11743f9ae7ea6d742b631b6d267b7dce47d Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 17 Feb 2026 08:46:37 -0500 Subject: [PATCH 5/5] Updates Error name in unit test --- tests/unit/test_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_flow.py b/tests/unit/test_flow.py index 0806460..2a898cc 100644 --- a/tests/unit/test_flow.py +++ b/tests/unit/test_flow.py @@ -508,7 +508,7 @@ def test_run_local_server_timeout( # handle_request does nothing (simulating timeout), so last_request_uri remains None mock_server.handle_request.return_value = None - with pytest.raises(flow.WSGITimeout): + with pytest.raises(flow.WSGITimeoutError): instance.run_local_server(timeout_seconds=1) webbrowser_mock.get.assert_called_with(None)