Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move login flows #1057

Merged
merged 6 commits into from
Sep 20, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add LoginFlowManagers to docs
derek-globus committed Sep 19, 2024

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
commit 2cc16e3afa1e366466cd4a85c71fea5bbd5e4bbc
1 change: 1 addition & 0 deletions docs/authorization/index.rst
Original file line number Diff line number Diff line change
@@ -8,4 +8,5 @@ Components of the Globus SDK which handle application authorization.

globus_authorizers
scopes_and_consents/index
login_flows
gare
100 changes: 100 additions & 0 deletions docs/authorization/login_flows.rst
Original file line number Diff line number Diff line change
@@ -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 <https://auth.globus.org/v2/web/developers>`_ 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
6 changes: 4 additions & 2 deletions src/globus_sdk/login_flows/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
63 changes: 31 additions & 32 deletions src/globus_sdk/login_flows/command_line_login_flow_manager.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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",
]
Original file line number Diff line number Diff line change
@@ -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).
"""
Original file line number Diff line number Diff line change
@@ -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.")
Loading