Skip to content

Commit

Permalink
fix: avoid deprecated "out-of-band" authentication flow (#186)
Browse files Browse the repository at this point in the history
* fix: avoid deprecated "out-of-band" authentication flow

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
tswast and gcf-owl-bot[bot] committed Mar 15, 2022
1 parent 82bb5dc commit f119c4e
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 3 deletions.
72 changes: 70 additions & 2 deletions google_auth_oauthlib/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,68 @@

from __future__ import absolute_import

import contextlib
import socket

import google_auth_oauthlib.flow


def get_user_credentials(scopes, client_id, client_secret):
LOCALHOST = "localhost"
DEFAULT_PORTS_TO_TRY = 100


def is_port_open(port):
"""Check if a port is open on localhost.
Based on StackOverflow answer: https://stackoverflow.com/a/43238489/101923
Parameters
----------
port : int
A port to check on localhost.
Returns
-------
is_open : bool
True if a socket can be opened at the requested port.
"""
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
try:
sock.bind((LOCALHOST, port))
sock.listen(1)
except socket.error:
is_open = False
else:
is_open = True
return is_open


def find_open_port(start=8080, stop=None):
"""Find an open port between ``start`` and ``stop``.
Parameters
----------
start : Optional[int]
Beginning of range of ports to try. Defaults to 8080.
stop : Optional[int]
End of range of ports to try (not including exactly equals ``stop``).
This function tries 100 possible ports if no ``stop`` is specified.
Returns
-------
Optional[int]
``None`` if no open port is found, otherwise an integer indicating an
open port.
"""
if not stop:
stop = start + DEFAULT_PORTS_TO_TRY

for port in range(start, stop):
if is_port_open(port):
return port

# No open ports found.
return None


def get_user_credentials(
scopes, client_id, client_secret, minimum_port=8080, maximum_port=None
):
"""Gets credentials associated with your Google user account.
This function authenticates using your user credentials by going through
Expand Down Expand Up @@ -53,6 +111,12 @@ def get_user_credentials(scopes, client_id, client_secret):
A string that verifies your application to Google APIs. Find this
value in the `Credentials page on the Google Developer's Console
<https://console.developers.google.com/apis/credentials>`_.
minimum_port (int):
Beginning of range of ports to try for redirect URI HTTP server.
Defaults to 8080.
maximum_port (Optional[int]):
End of range of ports to try (not including exactly equals ``stop``).
This function tries 100 possible ports if no ``stop`` is specified.
Returns:
google.oauth2.credentials.Credentials:
Expand Down Expand Up @@ -102,4 +166,8 @@ def get_user_credentials(scopes, client_id, client_secret):
client_config, scopes=scopes
)

return app_flow.run_console()
port = find_open_port()
if not port:
raise ConnectionError("Could not find open port.")

return app_flow.run_local_server(host=LOCALHOST, port=port)
63 changes: 62 additions & 1 deletion tests/unit/test_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,47 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import socket

import mock
import pytest


def test_find_open_port_finds_start_port(monkeypatch):
from google_auth_oauthlib import interactive as module_under_test

monkeypatch.setattr(socket, "socket", mock.create_autospec(socket.socket))
port = module_under_test.find_open_port(9999)
assert port == 9999


def test_find_open_port_finds_stop_port(monkeypatch):
from google_auth_oauthlib import interactive as module_under_test

socket_instance = mock.create_autospec(socket.socket, instance=True)

def mock_socket(family, type_):
return socket_instance

monkeypatch.setattr(socket, "socket", mock_socket)
socket_instance.listen.side_effect = [socket.error] * 99 + [None]
port = module_under_test.find_open_port(9000, stop=9100)
assert port == 9099


def test_find_open_port_returns_none(monkeypatch):
from google_auth_oauthlib import interactive as module_under_test

socket_instance = mock.create_autospec(socket.socket, instance=True)

def mock_socket(family, type_):
return socket_instance

monkeypatch.setattr(socket, "socket", mock_socket)
socket_instance.listen.side_effect = socket.error
port = module_under_test.find_open_port(9000)
assert port is None
socket_instance.listen.assert_has_calls(mock.call(1) for _ in range(100))


def test_get_user_credentials():
Expand All @@ -33,4 +73,25 @@ def test_get_user_credentials():
actual_client_config = mock_flow.from_client_config.call_args[0][0]
assert actual_client_config["installed"]["client_id"] == "some-client-id"
assert actual_client_config["installed"]["client_secret"] == "shh-secret"
mock_flow_instance.run_console.assert_called_once()
mock_flow_instance.run_local_server.assert_called_once()


def test_get_user_credentials_raises_connectionerror(monkeypatch):
from google_auth_oauthlib import flow
from google_auth_oauthlib import interactive as module_under_test

def mock_find_open_port():
return None

monkeypatch.setattr(module_under_test, "find_open_port", mock_find_open_port)
mock_flow = mock.create_autospec(flow.InstalledAppFlow, instance=True)

with mock.patch(
"google_auth_oauthlib.flow.InstalledAppFlow", autospec=True
) as mock_flow, pytest.raises(ConnectionError):
mock_flow.from_client_config.return_value = mock_flow
module_under_test.get_user_credentials(
["scopes"], "some-client-id", "shh-secret"
)

mock_flow.run_local_server.assert_not_called()

0 comments on commit f119c4e

Please sign in to comment.