From 2de369e1bb2aaa04e624da8eb88d6e3a7e00e40d Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Wed, 18 Sep 2024 12:07:55 -0500 Subject: [PATCH 1/6] Promote login flow managers from 'experimental' --- ...20240918_120618_derek_move_login_flows.rst | 8 ++++ pyproject.toml | 2 +- .../experimental/globus_app/_types.py | 2 +- .../experimental/globus_app/config.py | 10 ++--- .../experimental/globus_app/user_app.py | 5 +-- .../experimental/login_flow_manager.py | 37 +++++++++++++++++++ .../__init__.py | 8 +++- .../command_line_login_flow_manager.py | 0 .../__init__.py | 2 + .../_local_server.py | 2 +- .../html_files/__init__.py | 0 .../html_files/local_server_landing_page.html | 0 .../local_server_login_flow_manager.py | 4 +- .../login_flow_manager.py | 0 tests/functional/login_flows/__init__.py | 0 .../test_login_flow_manager.py} | 6 +-- .../globus_app/test_globus_app.py | 10 ++--- .../unit/experimental/test_legacy_support.py | 11 ++++++ tests/unit/login_flows/__init__.py | 0 .../test_local_server.py | 4 +- 20 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 changelog.d/20240918_120618_derek_move_login_flows.rst create mode 100644 src/globus_sdk/experimental/login_flow_manager.py rename src/globus_sdk/{experimental/login_flow_manager => login_flows}/__init__.py (64%) rename src/globus_sdk/{experimental/login_flow_manager => login_flows}/command_line_login_flow_manager.py (100%) rename src/globus_sdk/{experimental/login_flow_manager => login_flows}/local_server_login_flow_manager/__init__.py (64%) rename src/globus_sdk/{experimental/login_flow_manager => login_flows}/local_server_login_flow_manager/_local_server.py (96%) rename src/globus_sdk/{experimental/login_flow_manager => login_flows}/local_server_login_flow_manager/html_files/__init__.py (100%) rename src/globus_sdk/{experimental/login_flow_manager => login_flows}/local_server_login_flow_manager/html_files/local_server_landing_page.html (100%) rename src/globus_sdk/{experimental/login_flow_manager => login_flows}/local_server_login_flow_manager/local_server_login_flow_manager.py (98%) rename src/globus_sdk/{experimental/login_flow_manager => login_flows}/login_flow_manager.py (100%) create mode 100644 tests/functional/login_flows/__init__.py rename tests/functional/{test_login_manager.py => login_flows/test_login_flow_manager.py} (97%) create mode 100644 tests/unit/login_flows/__init__.py rename tests/unit/{experimental => login_flows}/test_local_server.py (91%) diff --git a/changelog.d/20240918_120618_derek_move_login_flows.rst b/changelog.d/20240918_120618_derek_move_login_flows.rst new file mode 100644 index 000000000..371f1b17a --- /dev/null +++ b/changelog.d/20240918_120618_derek_move_login_flows.rst @@ -0,0 +1,8 @@ + +Changed +~~~~~~~ + +- LoginFlowManagers have been moved from ``globus_sdk.experimental.login_flow_managers`` + to ``globus_sdk.login_flows``. (:pr:`NUMBER`) + + diff --git a/pyproject.toml b/pyproject.toml index 10d329928..74ba5fb26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ namespaces = false [tool.setuptools.package-data] globus_sdk = ["py.typed"] -"globus_sdk.experimental.login_flow_manager.local_server_login_flow_manager.html_files" = ["*.html"] +"globus_sdk.login_flows.local_server_login_flow_manager.html_files" = ["*.html"] [tool.setuptools.dynamic.version] attr = "globus_sdk.__version__" diff --git a/src/globus_sdk/experimental/globus_app/_types.py b/src/globus_sdk/experimental/globus_app/_types.py index 65a4df29b..5ffb3b651 100644 --- a/src/globus_sdk/experimental/globus_app/_types.py +++ b/src/globus_sdk/experimental/globus_app/_types.py @@ -5,8 +5,8 @@ from globus_sdk import AuthLoginClient from globus_sdk._types import UUIDLike -from globus_sdk.experimental.login_flow_manager import LoginFlowManager from globus_sdk.experimental.tokenstorage import TokenStorage +from globus_sdk.login_flows import LoginFlowManager if sys.version_info < (3, 8): from typing_extensions import Protocol, runtime_checkable diff --git a/src/globus_sdk/experimental/globus_app/config.py b/src/globus_sdk/experimental/globus_app/config.py index bfee95e6b..179a5036b 100644 --- a/src/globus_sdk/experimental/globus_app/config.py +++ b/src/globus_sdk/experimental/globus_app/config.py @@ -4,17 +4,17 @@ import typing as t from globus_sdk.config import get_environment_name -from globus_sdk.experimental.login_flow_manager import ( - CommandLineLoginFlowManager, - LocalServerLoginFlowManager, - LoginFlowManager, -) from globus_sdk.experimental.tokenstorage import ( JSONTokenStorage, MemoryTokenStorage, SQLiteTokenStorage, TokenStorage, ) +from globus_sdk.login_flows import ( + CommandLineLoginFlowManager, + LocalServerLoginFlowManager, + LoginFlowManager, +) from ._types import ( LoginFlowManagerProvider, diff --git a/src/globus_sdk/experimental/globus_app/user_app.py b/src/globus_sdk/experimental/globus_app/user_app.py index 76646e941..ebac0a943 100644 --- a/src/globus_sdk/experimental/globus_app/user_app.py +++ b/src/globus_sdk/experimental/globus_app/user_app.py @@ -9,11 +9,8 @@ NativeAppAuthClient, ) from globus_sdk._types import ScopeCollectionType, UUIDLike -from globus_sdk.experimental.login_flow_manager import ( - CommandLineLoginFlowManager, - LoginFlowManager, -) from globus_sdk.gare import GlobusAuthorizationParameters +from globus_sdk.login_flows import CommandLineLoginFlowManager, LoginFlowManager from ._types import LoginFlowManagerProvider from .app import GlobusApp diff --git a/src/globus_sdk/experimental/login_flow_manager.py b/src/globus_sdk/experimental/login_flow_manager.py new file mode 100644 index 000000000..c4edd67a4 --- /dev/null +++ b/src/globus_sdk/experimental/login_flow_manager.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import sys +import typing as t + +__all__ = ( + "CommandLineLoginFlowManager", + "LocalServerLoginFlowManager", + "LoginFlowManager", +) + +# legacy aliases +# (when accessed, these will emit deprecation warnings) +if t.TYPE_CHECKING: + from globus_sdk.login_flows import ( + CommandLineLoginFlowManager, + LocalServerLoginFlowManager, + LoginFlowManager, + ) +else: + + def __getattr__(name: str) -> t.Any: + import globus_sdk.login_flows as login_flows_module + from globus_sdk.exc import warn_deprecated + + warn_deprecated( + "'globus_sdk.experimental.login_flow_manager' has been renamed to " + "'globus_sdk.login_flows'. " + f"Importing '{name}' from `globus_sdk.experimental` is deprecated. " + f"Use `globus_sdk.login_flows.{name}` instead." + ) + + value = getattr(login_flows_module, name, None) + if value is None: + raise AttributeError(f"module {__name__} has no attribute {name}") + setattr(sys.modules[__name__], name, value) + return value diff --git a/src/globus_sdk/experimental/login_flow_manager/__init__.py b/src/globus_sdk/login_flows/__init__.py similarity index 64% rename from src/globus_sdk/experimental/login_flow_manager/__init__.py rename to src/globus_sdk/login_flows/__init__.py index c4663c4f2..961ac3fd6 100644 --- a/src/globus_sdk/experimental/login_flow_manager/__init__.py +++ b/src/globus_sdk/login_flows/__init__.py @@ -1,9 +1,13 @@ from .command_line_login_flow_manager import CommandLineLoginFlowManager -from .local_server_login_flow_manager import LocalServerLoginFlowManager +from .local_server_login_flow_manager import ( + LocalServerError, + LocalServerLoginFlowManager, +) from .login_flow_manager import LoginFlowManager __all__ = [ - "LoginFlowManager", "CommandLineLoginFlowManager", + "LocalServerError", "LocalServerLoginFlowManager", + "LoginFlowManager", ] diff --git a/src/globus_sdk/experimental/login_flow_manager/command_line_login_flow_manager.py b/src/globus_sdk/login_flows/command_line_login_flow_manager.py similarity index 100% rename from src/globus_sdk/experimental/login_flow_manager/command_line_login_flow_manager.py rename to src/globus_sdk/login_flows/command_line_login_flow_manager.py diff --git a/src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/__init__.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/__init__.py similarity index 64% rename from src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/__init__.py rename to src/globus_sdk/login_flows/local_server_login_flow_manager/__init__.py index 99b78ba8e..63cd34dcb 100644 --- a/src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/__init__.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/__init__.py @@ -1,5 +1,7 @@ +from ._local_server import LocalServerError from .local_server_login_flow_manager import LocalServerLoginFlowManager __all__ = [ + "LocalServerError", "LocalServerLoginFlowManager", ] diff --git a/src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/_local_server.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/_local_server.py similarity index 96% rename from src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/_local_server.py rename to src/globus_sdk/login_flows/local_server_login_flow_manager/_local_server.py index 48ac5530f..f946e4f3c 100644 --- a/src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/_local_server.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/_local_server.py @@ -22,7 +22,7 @@ else: # Python < 3.9 import importlib_resources -import globus_sdk.experimental.login_flow_manager.local_server_login_flow_manager.html_files as html_files # noqa: E501 +import globus_sdk.login_flows.local_server_login_flow_manager.html_files as html_files # noqa: E501 _IS_WINDOWS = os.name == "nt" diff --git a/src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/html_files/__init__.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/html_files/__init__.py similarity index 100% rename from src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/html_files/__init__.py rename to src/globus_sdk/login_flows/local_server_login_flow_manager/html_files/__init__.py diff --git a/src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/html_files/local_server_landing_page.html b/src/globus_sdk/login_flows/local_server_login_flow_manager/html_files/local_server_landing_page.html similarity index 100% rename from src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/html_files/local_server_landing_page.html rename to src/globus_sdk/login_flows/local_server_login_flow_manager/html_files/local_server_landing_page.html diff --git a/src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/local_server_login_flow_manager.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py similarity index 98% rename from src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/local_server_login_flow_manager.py rename to src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py index 52c698f55..e7b1d0006 100644 --- a/src/globus_sdk/experimental/login_flow_manager/local_server_login_flow_manager/local_server_login_flow_manager.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py @@ -8,10 +8,8 @@ from string import Template from globus_sdk import AuthLoginClient, GlobusSDKUsageError, OAuthTokenResponse -from globus_sdk.experimental.login_flow_manager.login_flow_manager import ( - LoginFlowManager, -) from globus_sdk.gare import GlobusAuthorizationParameters +from globus_sdk.login_flows.login_flow_manager import LoginFlowManager from ._local_server import ( DEFAULT_HTML_TEMPLATE, diff --git a/src/globus_sdk/experimental/login_flow_manager/login_flow_manager.py b/src/globus_sdk/login_flows/login_flow_manager.py similarity index 100% rename from src/globus_sdk/experimental/login_flow_manager/login_flow_manager.py rename to src/globus_sdk/login_flows/login_flow_manager.py diff --git a/tests/functional/login_flows/__init__.py b/tests/functional/login_flows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/test_login_manager.py b/tests/functional/login_flows/test_login_flow_manager.py similarity index 97% rename from tests/functional/test_login_manager.py rename to tests/functional/login_flows/test_login_flow_manager.py index 0533874ef..3a6605503 100644 --- a/tests/functional/test_login_manager.py +++ b/tests/functional/login_flows/test_login_flow_manager.py @@ -2,11 +2,11 @@ from globus_sdk import ConfidentialAppAuthClient, NativeAppAuthClient from globus_sdk._testing import load_response -from globus_sdk.experimental.login_flow_manager import ( +from globus_sdk.gare import GlobusAuthorizationParameters +from globus_sdk.login_flows import ( CommandLineLoginFlowManager, LocalServerLoginFlowManager, ) -from globus_sdk.gare import GlobusAuthorizationParameters def _mock_input(s): @@ -112,7 +112,7 @@ def wait_for_code(self): _LOCAL_SERVER_MODULE = ( - "globus_sdk.experimental.login_flow_manager.local_server_login_flow_manager." + "globus_sdk.login_flows.local_server_login_flow_manager." "local_server_login_flow_manager" ) diff --git a/tests/unit/experimental/globus_app/test_globus_app.py b/tests/unit/experimental/globus_app/test_globus_app.py index 968b1ec58..27c4ddf78 100644 --- a/tests/unit/experimental/globus_app/test_globus_app.py +++ b/tests/unit/experimental/globus_app/test_globus_app.py @@ -24,11 +24,6 @@ RefreshTokenAuthorizerFactory, UserApp, ) -from globus_sdk.experimental.login_flow_manager import ( - CommandLineLoginFlowManager, - LocalServerLoginFlowManager, - LoginFlowManager, -) from globus_sdk.experimental.tokenstorage import ( JSONTokenStorage, MemoryTokenStorage, @@ -36,6 +31,11 @@ TokenStorageData, ) from globus_sdk.gare import GlobusAuthorizationParameters +from globus_sdk.login_flows import ( + CommandLineLoginFlowManager, + LocalServerLoginFlowManager, + LoginFlowManager, +) from globus_sdk.scopes import AuthScopes, Scope from globus_sdk.services.auth import OAuthTokenResponse diff --git a/tests/unit/experimental/test_legacy_support.py b/tests/unit/experimental/test_legacy_support.py index 9cde41677..0d8b87ffd 100644 --- a/tests/unit/experimental/test_legacy_support.py +++ b/tests/unit/experimental/test_legacy_support.py @@ -9,6 +9,8 @@ can be deleted. """ +import pytest + def test_scope_importable_from_experimental(): from globus_sdk.experimental.scope_parser import ( # noqa: F401 @@ -16,3 +18,12 @@ def test_scope_importable_from_experimental(): ScopeCycleError, ScopeParseError, ) + + +def test_login_flow_manager_importable_from_experimental(): + with pytest.warns(DeprecationWarning): + from globus_sdk.experimental.login_flow_manager import ( # noqa: F401 + CommandLineLoginFlowManager, + LocalServerLoginFlowManager, + LoginFlowManager, + ) diff --git a/tests/unit/login_flows/__init__.py b/tests/unit/login_flows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/experimental/test_local_server.py b/tests/unit/login_flows/test_local_server.py similarity index 91% rename from tests/unit/experimental/test_local_server.py rename to tests/unit/login_flows/test_local_server.py index 492830c0c..a24019f40 100644 --- a/tests/unit/experimental/test_local_server.py +++ b/tests/unit/login_flows/test_local_server.py @@ -2,9 +2,9 @@ import pytest -from globus_sdk.experimental.login_flow_manager.local_server_login_flow_manager._local_server import ( # noqa: E501 +from globus_sdk.login_flows import LocalServerError +from globus_sdk.login_flows.local_server_login_flow_manager._local_server import ( # noqa: E501 DEFAULT_HTML_TEMPLATE, - LocalServerError, RedirectHandler, RedirectHTTPServer, ) From 2cc16e3afa1e366466cd4a85c71fea5bbd5e4bbc Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Thu, 19 Sep 2024 14:21:01 -0500 Subject: [PATCH 2/6] Add LoginFlowManagers to docs --- docs/authorization/index.rst | 1 + docs/authorization/login_flows.rst | 100 ++++++++++++++++++ src/globus_sdk/login_flows/__init__.py | 6 +- .../command_line_login_flow_manager.py | 63 ++++++----- .../__init__.py | 5 +- .../local_server_login_flow_manager/errors.py | 9 ++ .../{_local_server.py => local_server.py} | 82 +++++++------- .../local_server_login_flow_manager.py | 86 +++++++-------- .../login_flows/login_flow_manager.py | 24 +++-- tests/unit/login_flows/test_local_server.py | 10 +- 10 files changed, 244 insertions(+), 142 deletions(-) create mode 100644 docs/authorization/login_flows.rst create mode 100644 src/globus_sdk/login_flows/local_server_login_flow_manager/errors.py rename src/globus_sdk/login_flows/local_server_login_flow_manager/{_local_server.py => local_server.py} (89%) diff --git a/docs/authorization/index.rst b/docs/authorization/index.rst index 2ac5118fd..62084a691 100644 --- a/docs/authorization/index.rst +++ b/docs/authorization/index.rst @@ -8,4 +8,5 @@ Components of the Globus SDK which handle application authorization. globus_authorizers scopes_and_consents/index + login_flows gare diff --git a/docs/authorization/login_flows.rst b/docs/authorization/login_flows.rst new file mode 100644 index 000000000..f90f9348f --- /dev/null +++ b/docs/authorization/login_flows.rst @@ -0,0 +1,100 @@ + +Login Flow Managers +=================== + +.. currentmodule:: globus_sdk.login_flows + +This page provides references for the LoginFlowManager abstract class and some concrete +implementations. + +A login flow manager is a class responsible for driving a user through a login flow, +with the ultimate goal of obtaining tokens. The tokens are required to make +requests against any Globus services. + +Interface +--------- + +.. autoclass:: LoginFlowManager + :members: + +Command Line +------------ + +As the name might suggest, a CommandLineLoginFlowManager drives user logins through +the command line (stdin/stdout). When run, the manager will print a URL to the console +then prompt a user to navigate to that url and enter the resulting auth code +back into the terminal. + +Example Code: + +.. code-block:: pycon + + >>> from globus_sdk import NativeAppAuthClient + >>> from globus_sdk.scopes import TransferScopes + >>> from globus_sdk.login_flows import CommandLineLoginFlowManager + + >>> login_client = NativeAppAuthClient(client_id=client_id) + >>> manager = CommandLineLoginFlowManager(login_client) + >>> + >>> token_response = manager.run_login_flow( + ... GlobusAuthorizationParameters(required_scopes=[TransferScopes.all]) + ... ) + Please authenticate with Globus here: + ------------------------------------- + https://auth.globus.org/v2/oauth2/authorize?cli...truncated... + ------------------------------------- + + Enter the resulting Authorization Code here: + +.. autoclass:: CommandLineLoginFlowManager + :members: + :member-order: bysource + :show-inheritance: + +Local Server +------------ + +A LocalServerLoginFlowManager drives more automated but less portable login flows +compared with its command line counterpart. When run, rather than printing the +authorization URL, the manager will open it in the user's default browser. Alongside +this, the manager will start a local web server to receive the auth code upon completion +of the login flow. + +This provides a more user-friendly login experience as there is no manually copy/pasting +of links and codes but also requires that the python process is running in an +environment with access to a supported browser. This flow is not suitable for headless +environments (e.g., while ssh-ed into a cluster node). + +.. warning:: + + Globus Auth requires that redirect URIs, including the local server used + here, be pre-registered with the client in use. + + Before using this flow, navigate to the + `Globus Developers Pane `_ and ensure + that `https://localhost` is listed as an allowed "Redirect URL" for your client. + + +Example Usage: + +.. code-block:: pycon + + >>> from globus_sdk import NativeAppAuthClient + >>> from globus_sdk.scopes import TransferScopes + >>> from globus_sdk.login_flows import LocalServerLoginFlowManager + + >>> login_client = NativeAppAuthClient(client_id=client_id) + >>> manager = LocalServerLoginFlowManager(login_client) + >>> + >>> token_response = manager.run_login_flow( + ... GlobusAuthorizationParameters(required_scopes=[TransferScopes.all]) + ... ) + +.. autoclass:: LocalServerLoginFlowManager + :members: + :member-order: bysource + :show-inheritance: + +.. autoexception:: LocalServerLoginError + +.. autoexception:: LocalServerEnvironmentalLoginError diff --git a/src/globus_sdk/login_flows/__init__.py b/src/globus_sdk/login_flows/__init__.py index 961ac3fd6..37c4b05cd 100644 --- a/src/globus_sdk/login_flows/__init__.py +++ b/src/globus_sdk/login_flows/__init__.py @@ -1,13 +1,15 @@ from .command_line_login_flow_manager import CommandLineLoginFlowManager from .local_server_login_flow_manager import ( - LocalServerError, + LocalServerEnvironmentalLoginError, + LocalServerLoginError, LocalServerLoginFlowManager, ) from .login_flow_manager import LoginFlowManager __all__ = [ "CommandLineLoginFlowManager", - "LocalServerError", + "LocalServerLoginError", + "LocalServerEnvironmentalLoginError", "LocalServerLoginFlowManager", "LoginFlowManager", ] diff --git a/src/globus_sdk/login_flows/command_line_login_flow_manager.py b/src/globus_sdk/login_flows/command_line_login_flow_manager.py index 93535c629..d8100675d 100644 --- a/src/globus_sdk/login_flows/command_line_login_flow_manager.py +++ b/src/globus_sdk/login_flows/command_line_login_flow_manager.py @@ -19,17 +19,22 @@ class CommandLineLoginFlowManager(LoginFlowManager): """ - A ``CommandLineLoginFlowManager`` is a ``LoginFlowManager`` that uses the command - line for interacting with the user during its interactive login flows. - - Example usage: - - >>> login_client = globus_sdk.NativeAppAuthClient(...) - >>> login_flow_manager = CommandLineLoginFlowManager(login_client) - >>> scopes = [globus_sdk.scopes.TransferScopes.all] - >>> auth_params = GlobusAuthorizationParameters(required_scopes=scopes) - >>> tokens = login_flow_manager.run_login_flow(auth_params) - + A login flow manager which drives authorization-code token grants through the + command line. + + :param AuthLoginClient login_client: The client that will be making Globus + Auth API calls required for a login flow. + + .. note:: + If this client is a :class:`globus_sdk.ConfidentialAppAuthClient`, an + explicit `redirect_uri` param is required. + + :param str redirect_uri: The redirect URI to use for the login flow. When the + `login_client` is a native client, this defaults to a globus-hosted URI. + :param bool request_refresh_tokens: A signal of whether refresh tokens are expected + to be requested, in addition to access tokens. + :param str native_prefill_named_grant: A string to prefill in a Native App login + flow. This value is only used if the `login_client` is a native client. """ def __init__( @@ -40,18 +45,6 @@ def __init__( request_refresh_tokens: bool = False, native_prefill_named_grant: str | None = None, ) -> None: - """ - :param login_client: The ``AuthLoginClient`` that will be making the Globus - Auth API calls needed for the authentication flow. Note that this - must either be a NativeAppAuthClient or a templated - ConfidentialAppAuthClient, standard ConfidentialAppAuthClients cannot - use the web auth-code flow. - :param redirect_uri: The redirect URI to use for the login flow. Defaults to - a globus-hosted helper web auth-code URI for NativeAppAuthClients. - :param request_refresh_tokens: Control whether refresh tokens will be requested. - :param native_prefill_named_grant: The named grant label to prefill on the - consent page when using a NativeAppAuthClient. - """ super().__init__( login_client, request_refresh_tokens=request_refresh_tokens, @@ -73,15 +66,13 @@ def for_globus_app( cls, app_name: str, login_client: AuthLoginClient, config: GlobusAppConfig ) -> CommandLineLoginFlowManager: """ - Create a ``CommandLineLoginFlowManager`` for a given ``GlobusAppConfig``. - - :param app_name: The name of the app to use for prefilling the named grant in - native auth flows. - :param login_client: The ``AuthLoginClient`` to use to drive Globus Auth flows. - :param config: A ``GlobusAppConfig`` to configure the login flow. - :returns: A ``CommandLineLoginFlowManager`` instance. - :raises: GlobusSDKUsageError if a login_redirect_uri is not set on the config - but a ConfidentialAppAuthClient is used. + Creates a ``CommandLineLoginFlowManager`` for use in a GlobusApp. + + :param app_name: The name of the app. Will be prefilled in native auth flows. + :param login_client: A client used to make Globus Auth API calls. + :param config: A GlobusApp-bounded object used to configure login flow manager. + :raises: GlobusSDKUsageError if login_redirect_uri is not set on the config + but a ConfidentialAppAuthClient is supplied. """ return cls( login_client, @@ -110,6 +101,9 @@ def print_authorize_url(self, authorize_url: str) -> None: """ Prompt the user to authenticate using the provided ``authorize_url``. + This method is publicly exposed to allow for simpler customization through + subclassing and overriding. + :param authorize_url: The URL at which the user will login and consent to application accesses. """ @@ -128,6 +122,11 @@ def print_authorize_url(self, authorize_url: str) -> None: def prompt_for_code(self) -> str: """ Prompt the user to enter an authorization code. + + This method is publicly exposed to allow for simpler customization through + subclassing and overriding. + + :returns str: The authorization code entered by the user. """ code_prompt = "Enter the resulting Authorization Code here: " return input(code_prompt).strip() diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/__init__.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/__init__.py index 63cd34dcb..4bc359bad 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/__init__.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/__init__.py @@ -1,7 +1,8 @@ -from ._local_server import LocalServerError +from .errors import LocalServerEnvironmentalLoginError, LocalServerLoginError from .local_server_login_flow_manager import LocalServerLoginFlowManager __all__ = [ - "LocalServerError", + "LocalServerLoginError", + "LocalServerEnvironmentalLoginError", "LocalServerLoginFlowManager", ] diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/errors.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/errors.py new file mode 100644 index 000000000..58c629452 --- /dev/null +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/errors.py @@ -0,0 +1,9 @@ +class LocalServerLoginError(Exception): + """An error raised during a LocalServerLoginFlowManager's run.""" + + +class LocalServerEnvironmentalLoginError(LocalServerLoginError): + """ + Error raised when a local server login flow fails to start due to incompatible + environment conditions (e.g., a remote session or text-only browser). + """ diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/_local_server.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server.py similarity index 89% rename from src/globus_sdk/login_flows/local_server_login_flow_manager/_local_server.py rename to src/globus_sdk/login_flows/local_server_login_flow_manager/local_server.py index f946e4f3c..ee32360be 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/_local_server.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server.py @@ -13,10 +13,13 @@ import sys import time import typing as t +from datetime import timedelta from http.server import BaseHTTPRequestHandler, HTTPServer from string import Template from urllib.parse import parse_qsl, urlparse +from .errors import LocalServerLoginError + if sys.version_info >= (3, 9): import importlib.resources as importlib_resources else: # Python < 3.9 @@ -33,19 +36,50 @@ ) -class LocalServerError(Exception): +class RedirectHandler(BaseHTTPRequestHandler): """ - Error class for errors raised by the local server when using a - LocalServerLoginFlowManager + BaseHTTPRequestHandler to be used by RedirectHTTPServer. + Displays the RedirectHTTPServer's html_template and parses auth_code out of + the redirect url. """ + server: RedirectHTTPServer + + def do_GET(self) -> None: + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + html_template = self.server.html_template + query_params = dict(parse_qsl(urlparse(self.path).query)) + code = query_params.get("code") + if code: + self.wfile.write( + html_template.substitute( + post_login_message="", login_result="Login successful" + ).encode("utf-8") + ) + self.server.return_code(code) + else: + msg = query_params.get("error_description", query_params.get("error")) + + self.wfile.write( + html_template.substitute( + post_login_message=msg, login_result="Login failed" + ).encode("utf-8") + ) + + self.server.return_code(LocalServerLoginError(msg)) + class RedirectHTTPServer(HTTPServer): """ - HTTPServer that accepts an html_template to be displayed to the user + An HTTPServer which accepts a html_template to be displayed to the user and uses a Queue to receive an auth_code from its RequestHandler. """ + WAIT_TIMEOUT = timedelta(minutes=5) + def __init__( self, server_address: tuple[str, int], @@ -72,7 +106,7 @@ def return_code(self, code: str | BaseException) -> None: def wait_for_code(self) -> str | BaseException: # Windows needs special handling as blocking prevents ctrl-c interrupts if _IS_WINDOWS: - deadline = time.time() + 3600 + deadline = time.time() + self.WAIT_TIMEOUT.total_seconds() while time.time() < deadline: try: return self._auth_code_queue.get() @@ -83,40 +117,4 @@ def wait_for_code(self) -> str | BaseException: return self._auth_code_queue.get(block=True, timeout=3600) except queue.Empty: pass - raise LocalServerError("Login timed out. Please try again.") - - -class RedirectHandler(BaseHTTPRequestHandler): - """ - BaseHTTPRequestHandler to be used by RedirectHTTPServer. - Displays the RedirectHTTPServer's html_template and parses auth_code out of - the redirect url. - """ - - server: RedirectHTTPServer - - def do_GET(self) -> None: - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - - html_template = self.server.html_template - query_params = dict(parse_qsl(urlparse(self.path).query)) - code = query_params.get("code") - if code: - self.wfile.write( - html_template.substitute( - post_login_message="", login_result="Login successful" - ).encode("utf-8") - ) - self.server.return_code(code) - else: - msg = query_params.get("error_description", query_params.get("error")) - - self.wfile.write( - html_template.substitute( - post_login_message=msg, login_result="Login failed" - ).encode("utf-8") - ) - - self.server.return_code(LocalServerError(msg)) + raise LocalServerLoginError("Login timed out. Please try again.") diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py index e7b1d0006..cbd5bf22d 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py @@ -11,9 +11,10 @@ from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows.login_flow_manager import LoginFlowManager -from ._local_server import ( +from .errors import LocalServerEnvironmentalLoginError +from .local_server import ( DEFAULT_HTML_TEMPLATE, - LocalServerError, + LocalServerLoginError, RedirectHandler, RedirectHTTPServer, ) @@ -29,22 +30,13 @@ BROWSER_BLACKLIST = ["lynx", "www-browser", "links", "elinks", "w3m"] -class LocalServerLoginFlowError(BaseException): - """ - Error class for errors raised due to inability to run a local server login flow - due to known failure conditions such as remote sessions or text-only browsers. - Catching this should be sufficient to detect cases where one should fallback - to a CommandLineLoginFlowManager - """ - - def _check_remote_session() -> None: """ Try to check if this is being run during a remote session, if so raise LocalServerLoginFlowError """ if bool(os.environ.get("SSH_TTY", os.environ.get("SSH_CONNECTION"))): - raise LocalServerLoginFlowError( + raise LocalServerEnvironmentalLoginError( "Cannot use LocalServerLoginFlowManager in a remote session" ) @@ -64,34 +56,42 @@ def _open_webbrowser(url: str) -> None: # https://github.com/python/cpython/issues/82828 browser_name = browser._name else: - raise LocalServerLoginFlowError("Unable to determine local browser name.") + raise LocalServerEnvironmentalLoginError( + "Unable to determine local browser name." + ) if browser_name in BROWSER_BLACKLIST: - raise LocalServerLoginFlowError( + raise LocalServerEnvironmentalLoginError( "Cannot use LocalServerLoginFlowManager with " f"text-only browser '{browser_name}'" ) if not browser.open(url, new=1): - raise LocalServerLoginFlowError(f"Failed to open browser '{browser_name}'") + raise LocalServerEnvironmentalLoginError( + f"Failed to open browser '{browser_name}'" + ) except webbrowser.Error as exc: - raise LocalServerLoginFlowError("Failed to open browser") from exc + raise LocalServerEnvironmentalLoginError("Failed to open browser") from exc class LocalServerLoginFlowManager(LoginFlowManager): """ - A ``LocalServerLoginFlowManager`` is a ``LoginFlowManager`` that uses a locally - hosted server to automatically receive the auth code from Globus auth after the - user has authenticated. - - Example usage: - - >>> login_client = globus_sdk.NativeAppAuthClient(...) - >>> login_flow_manager = LocalServerLoginFlowManager(login_client) - >>> scopes = [globus_sdk.scopes.TransferScopes.all] - >>> auth_params = GlobusAuthorizationParameters(required_scopes=scopes) - >>> tokens = login_flow_manager.run_login_flow(auth_params) - + A login flow manager which uses a locally hosted server to drive authentication-code + token grants. The local server is used as the authorization redirect URI, + automatically receiving the auth code from Globus Auth after authentication/consent. + + :param AuthLoginClient login_client: The client that will be making Globus + Auth API calls required for a login flow. + :param bool request_refresh_tokens: A signal of whether refresh tokens are expected + to be requested, in addition to access tokens. + :param str native_prefill_named_grant: A string to prefill in a Native App login + flow. This value is only used if the `login_client` is a native client. + :param Template html_template: Optional HTML Template to be populated with the + values login_result and post_login_message and displayed to the user. A simple + default is supplied if not provided which informs the user that the login was + successful and that they may close the browser window. + :param tuple[str, int] server_address: Optional tuple of the form (host, port) to + specify an address to run the local server at. Defaults to ("127.0.0.1", 0). """ def __init__( @@ -103,20 +103,6 @@ def __init__( server_address: tuple[str, int] = ("127.0.0.1", 0), html_template: Template = DEFAULT_HTML_TEMPLATE, ) -> None: - """ - :param login_client: The ``AuthLoginClient`` that will be making the Globus - Auth API calls needed for the authentication flow. Note that this - must either be a NativeAppAuthClient or a templated - ConfidentialAppAuthClient, standard ConfidentialAppAuthClients cannot - use the web auth-code flow. - :param request_refresh_tokens: Control whether refresh tokens will be requested. - :param native_prefill_named_grant: The named grant label to prefill on the - consent page when using a NativeAppAuthClient. - :param html_template: Optional HTML Template to be populated with the values - login_result and post_login_message and displayed to the user. - :param server_address: Optional tuple of the form (host, port) to specify an - address to run the local server at. - """ super().__init__( login_client, request_refresh_tokens=request_refresh_tokens, @@ -130,12 +116,11 @@ def for_globus_app( cls, app_name: str, login_client: AuthLoginClient, config: GlobusAppConfig ) -> LocalServerLoginFlowManager: """ - Create a ``LocalServerLoginFlowManager`` for a given ``GlobusAppConfig``. + Create a ``LocalServerLoginFlowManager`` for use in a GlobusApp. - :param app_name: The name of the app to use for prefilling the named grant,. - :param login_client: The ``AuthLoginClient`` to use to drive Globus Auth flows. - :param config: A ``GlobusAppConfig`` to configure the login flow. - :returns: A ``LocalServerLoginFlowManager`` instance. + :param app_name: The name of the app. Will be prefilled in native auth flows. + :param login_client: A client used to make Globus Auth API calls. + :param config: A GlobusApp-bounded object used to configure login flow manager. :raises: GlobusSDKUsageError if a custom login_redirect_uri is defined in the config. """ @@ -161,6 +146,11 @@ def run_login_flow( :param auth_parameters: ``GlobusAuthorizationParameters`` passed through to the authentication flow to control how the user will authenticate. + :raises LocalServerEnvironmentalLoginError: If the local server login flow + cannot be run due to known failure conditions such as remote sessions or + text-only browsers. + :raises LocalServerLoginError: If the local server login flow fails for any + reason. """ _check_remote_session() @@ -177,7 +167,7 @@ def run_login_flow( if isinstance(auth_code, BaseException): msg = f"Authorization failed with unexpected error:\n{auth_code}." - raise LocalServerError(msg) + raise LocalServerLoginError(msg) # get and return tokens return self.login_client.oauth2_exchange_code_for_tokens(auth_code) diff --git a/src/globus_sdk/login_flows/login_flow_manager.py b/src/globus_sdk/login_flows/login_flow_manager.py index c31f6abf8..21a96ae62 100644 --- a/src/globus_sdk/login_flows/login_flow_manager.py +++ b/src/globus_sdk/login_flows/login_flow_manager.py @@ -14,9 +14,18 @@ class LoginFlowManager(metaclass=abc.ABCMeta): """ - A ``LoginFlowManager`` is an abstract superclass for subclasses that manage - interactive login flows with a user in order to authenticate with Globus Auth - and obtain tokens. + The abstract base class defining the interface for managing login flows. + + Implementing classes must supply a ``run_login_flow`` method. + Utility functions starting an authorization-code grant flow and getting an + authorization-code URL are provided on the class. + + :ivar AuthLoginClient login_client: A native or confidential login client to be + used by the login flow manager. + :ivar bool request_refresh_tokens: A signal of whether refresh tokens are expected + to be requested, in addition to access tokens. + :ivar str native_prefill_named_grant: A string to prefill in a Native App login + flow. This value is only to be used if the `login_client` is a native client. """ def __init__( @@ -26,13 +35,6 @@ def __init__( request_refresh_tokens: bool = False, native_prefill_named_grant: str | None = None, ) -> None: - """ - :param login_client: The client to use for login flows. - :param request_refresh_tokens: Control whether refresh tokens will be requested. - :param native_prefill_named_grant: The name of a prefill in a Native App login - flow. This value will be ignored if the login_client is not a - NativeAppAuthClient. - """ if not isinstance(login_client, NativeAppAuthClient) and not isinstance( login_client, ConfidentialAppAuthClient ): @@ -94,7 +96,7 @@ def run_login_flow( auth_parameters: GlobusAuthorizationParameters, ) -> OAuthTokenResponse: """ - Run an interactive login flow to get tokens for the user. + Run a login flow to get tokens for a user. :param auth_parameters: ``GlobusAuthorizationParameters`` passed through to the authentication flow to control how the user will authenticate. diff --git a/tests/unit/login_flows/test_local_server.py b/tests/unit/login_flows/test_local_server.py index a24019f40..df863cda1 100644 --- a/tests/unit/login_flows/test_local_server.py +++ b/tests/unit/login_flows/test_local_server.py @@ -2,8 +2,8 @@ import pytest -from globus_sdk.login_flows import LocalServerError -from globus_sdk.login_flows.local_server_login_flow_manager._local_server import ( # noqa: E501 +from globus_sdk.login_flows import LocalServerLoginError +from globus_sdk.login_flows.local_server_login_flow_manager.local_server import ( # noqa: E501 DEFAULT_HTML_TEMPLATE, RedirectHandler, RedirectHTTPServer, @@ -21,8 +21,8 @@ def test_default_html_template_contains_expected_text(): "url,expected_result", [ (b"localhost?code=abc123", "abc123"), - (b"localhost?error=bad_login", LocalServerError("bad_login")), - (b"localhost", LocalServerError(None)), + (b"localhost?error=bad_login", LocalServerLoginError("bad_login")), + (b"localhost", LocalServerLoginError(None)), ], ) def test_server(url, expected_result): @@ -54,7 +54,7 @@ def test_server(url, expected_result): result = server.wait_for_code() if isinstance(result, str): assert result == expected_result - elif isinstance(result, LocalServerError): + elif isinstance(result, LocalServerLoginError): assert result.args == expected_result.args else: raise AssertionError("unexpected result type") From 9666deb1ef2a0e3f96c2b95fe3116997e15915c2 Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Thu, 19 Sep 2024 14:26:40 -0500 Subject: [PATCH 3/6] Fix import path --- .../local_server_login_flow_manager.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py index cbd5bf22d..c16cbfbe4 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py @@ -11,13 +11,8 @@ from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows.login_flow_manager import LoginFlowManager -from .errors import LocalServerEnvironmentalLoginError -from .local_server import ( - DEFAULT_HTML_TEMPLATE, - LocalServerLoginError, - RedirectHandler, - RedirectHTTPServer, -) +from .errors import LocalServerEnvironmentalLoginError, LocalServerLoginError +from .local_server import DEFAULT_HTML_TEMPLATE, RedirectHandler, RedirectHTTPServer if t.TYPE_CHECKING: from globus_sdk.experimental.globus_app import GlobusAppConfig From 1bd9602a26637b387fbcf70cda73deb068dc727d Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Fri, 20 Sep 2024 11:20:00 -0500 Subject: [PATCH 4/6] Cosmetic changes --- docs/authorization/login_flows.rst | 10 +++++----- .../login_flows/command_line_login_flow_manager.py | 12 +++--------- .../local_server_login_flow_manager/local_server.py | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/authorization/login_flows.rst b/docs/authorization/login_flows.rst index f90f9348f..2e9fce160 100644 --- a/docs/authorization/login_flows.rst +++ b/docs/authorization/login_flows.rst @@ -22,7 +22,7 @@ Command Line As the name might suggest, a CommandLineLoginFlowManager drives user logins through the command line (stdin/stdout). When run, the manager will print a URL to the console -then prompt a user to navigate to that url and enter the resulting auth code +then prompt a user to navigate to that URL and enter the resulting auth code back into the terminal. Example Code: @@ -54,16 +54,16 @@ Example Code: Local Server ------------ -A LocalServerLoginFlowManager drives more automated but less portable login flows +A LocalServerLoginFlowManager drives more automated, but less portable, login flows compared with its command line counterpart. When run, rather than printing the authorization URL, the manager will open it in the user's default browser. Alongside this, the manager will start a local web server to receive the auth code upon completion of the login flow. This provides a more user-friendly login experience as there is no manually copy/pasting -of links and codes but also requires that the python process is running in an -environment with access to a supported browser. This flow is not suitable for headless -environments (e.g., while ssh-ed into a cluster node). +of links and codes. It also requires however that the python process be running in an +environment with access to a supported browser. As such, this flow is not suitable for +headless environments (e.g., while ssh-ed into a cluster node). .. warning:: diff --git a/src/globus_sdk/login_flows/command_line_login_flow_manager.py b/src/globus_sdk/login_flows/command_line_login_flow_manager.py index d8100675d..c0b5fdc53 100644 --- a/src/globus_sdk/login_flows/command_line_login_flow_manager.py +++ b/src/globus_sdk/login_flows/command_line_login_flow_manager.py @@ -30,7 +30,7 @@ class CommandLineLoginFlowManager(LoginFlowManager): explicit `redirect_uri` param is required. :param str redirect_uri: The redirect URI to use for the login flow. When the - `login_client` is a native client, this defaults to a globus-hosted URI. + `login_client` is a native client, this defaults to a Globus-hosted URL. :param bool request_refresh_tokens: A signal of whether refresh tokens are expected to be requested, in addition to access tokens. :param str native_prefill_named_grant: A string to prefill in a Native App login @@ -66,7 +66,7 @@ def for_globus_app( cls, app_name: str, login_client: AuthLoginClient, config: GlobusAppConfig ) -> CommandLineLoginFlowManager: """ - Creates a ``CommandLineLoginFlowManager`` for use in a GlobusApp. + Create a ``CommandLineLoginFlowManager`` for use in a GlobusApp. :param app_name: The name of the app. Will be prefilled in native auth flows. :param login_client: A client used to make Globus Auth API calls. @@ -101,9 +101,6 @@ def print_authorize_url(self, authorize_url: str) -> None: """ Prompt the user to authenticate using the provided ``authorize_url``. - This method is publicly exposed to allow for simpler customization through - subclassing and overriding. - :param authorize_url: The URL at which the user will login and consent to application accesses. """ @@ -123,10 +120,7 @@ def prompt_for_code(self) -> str: """ Prompt the user to enter an authorization code. - This method is publicly exposed to allow for simpler customization through - subclassing and overriding. - - :returns str: The authorization code entered by the user. + :returns: The authorization code entered by the user. """ code_prompt = "Enter the resulting Authorization Code here: " return input(code_prompt).strip() diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server.py index ee32360be..88caaf5d3 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server.py @@ -74,11 +74,11 @@ def do_GET(self) -> None: class RedirectHTTPServer(HTTPServer): """ - An HTTPServer which accepts a html_template to be displayed to the user + An HTTPServer which accepts an HTML `Template` to be displayed to the user and uses a Queue to receive an auth_code from its RequestHandler. """ - WAIT_TIMEOUT = timedelta(minutes=5) + WAIT_TIMEOUT = timedelta(minutes=15) def __init__( self, From 0906e9001fcfdffe5c6053239182137ef9e634ee Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Fri, 20 Sep 2024 12:39:49 -0500 Subject: [PATCH 5/6] Only allow native apps to use localserver login flow manager --- docs/authorization/login_flows.rst | 9 ++------- .../local_server_login_flow_manager.py | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/authorization/login_flows.rst b/docs/authorization/login_flows.rst index 2e9fce160..c1abde809 100644 --- a/docs/authorization/login_flows.rst +++ b/docs/authorization/login_flows.rst @@ -65,14 +65,9 @@ of links and codes. It also requires however that the python process be running environment with access to a supported browser. As such, this flow is not suitable for headless environments (e.g., while ssh-ed into a cluster node). -.. warning:: +.. note:: - Globus Auth requires that redirect URIs, including the local server used - here, be pre-registered with the client in use. - - Before using this flow, navigate to the - `Globus Developers Pane `_ and ensure - that `https://localhost` is listed as an allowed "Redirect URL" for your client. + This login manager is only supported for native clients. Example Usage: diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py index c16cbfbe4..e81dfdb2b 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py @@ -7,7 +7,12 @@ from contextlib import contextmanager from string import Template -from globus_sdk import AuthLoginClient, GlobusSDKUsageError, OAuthTokenResponse +from globus_sdk import ( + AuthLoginClient, + GlobusSDKUsageError, + NativeAppAuthClient, + OAuthTokenResponse, +) from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows.login_flow_manager import LoginFlowManager @@ -95,7 +100,7 @@ def __init__( *, request_refresh_tokens: bool = False, native_prefill_named_grant: str | None = None, - server_address: tuple[str, int] = ("127.0.0.1", 0), + server_address: tuple[str, int] = ("localhost", 0), html_template: Template = DEFAULT_HTML_TEMPLATE, ) -> None: super().__init__( @@ -116,14 +121,20 @@ def for_globus_app( :param app_name: The name of the app. Will be prefilled in native auth flows. :param login_client: A client used to make Globus Auth API calls. :param config: A GlobusApp-bounded object used to configure login flow manager. - :raises: GlobusSDKUsageError if a custom login_redirect_uri is defined in - the config. + :raises: GlobusSDKUsageError if app config is incompatible with the manager. """ if config.login_redirect_uri: # A "local server" relies on the user being redirected back to the server # running on the local machine, so it can't use a custom redirect URI. msg = "Cannot define a custom redirect_uri for LocalServerLoginFlowManager." raise GlobusSDKUsageError(msg) + if not isinstance(login_client, NativeAppAuthClient): + # Globus Auth has special provisions for native clients which allow implicit + # redirect url grant to localhost:. This is required for the + # LocalServerLoginFlowManager to work and is not reproducible in + # confidential clients. + msg = "LocalServerLoginFlowManager is only supported for Native Apps." + raise GlobusSDKUsageError(msg) return cls( login_client, From 33412f68b510fc9c51ca07b0ca0451266912bcba Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Fri, 20 Sep 2024 13:04:16 -0500 Subject: [PATCH 6/6] Fix raises class references --- src/globus_sdk/login_flows/command_line_login_flow_manager.py | 2 +- .../local_server_login_flow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/globus_sdk/login_flows/command_line_login_flow_manager.py b/src/globus_sdk/login_flows/command_line_login_flow_manager.py index c0b5fdc53..5d89d807b 100644 --- a/src/globus_sdk/login_flows/command_line_login_flow_manager.py +++ b/src/globus_sdk/login_flows/command_line_login_flow_manager.py @@ -71,7 +71,7 @@ def for_globus_app( :param app_name: The name of the app. Will be prefilled in native auth flows. :param login_client: A client used to make Globus Auth API calls. :param config: A GlobusApp-bounded object used to configure login flow manager. - :raises: GlobusSDKUsageError if login_redirect_uri is not set on the config + :raises GlobusSDKUsageError: if login_redirect_uri is not set on the config but a ConfidentialAppAuthClient is supplied. """ return cls( diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py index e81dfdb2b..2d86e981a 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py @@ -121,7 +121,7 @@ def for_globus_app( :param app_name: The name of the app. Will be prefilled in native auth flows. :param login_client: A client used to make Globus Auth API calls. :param config: A GlobusApp-bounded object used to configure login flow manager. - :raises: GlobusSDKUsageError if app config is incompatible with the manager. + :raises GlobusSDKUsageError: if app config is incompatible with the manager. """ if config.login_redirect_uri: # A "local server" relies on the user being redirected back to the server