diff --git a/examples/auth-session-management/app_managed_auth_state__app_custom_storage__oauth_user_authcode__with_browser.py b/examples/auth-session-management/app_managed_auth_state__app_custom_storage__oauth_user_authcode__with_browser.py index cdbfe6577..edfcb30aa 100644 --- a/examples/auth-session-management/app_managed_auth_state__app_custom_storage__oauth_user_authcode__with_browser.py +++ b/examples/auth-session-management/app_managed_auth_state__app_custom_storage__oauth_user_authcode__with_browser.py @@ -91,10 +91,12 @@ def example_main(): storage_provider=DemoStorageProvider(), ) - # In contrast to an in-memory only application that must initialize a login every - # time, an app with persistent storage can skip this when it is not needed. - if not plsdk_auth.is_initialized(): - plsdk_auth.user_login(allow_open_browser=True) + # In contrast to in-memory applications that must initialize a login every + # time, an app with persistent storage can skip user prompts when they + # are not needed. + # This helper will prompt the user only when it is necessary. + plsdk_auth.ensure_initialized(allow_open_browser=True, + allow_tty_prompt=True) # Create a Planet SDK object that uses the loaded auth session. sess = planet.Session(plsdk_auth) diff --git a/examples/auth-session-management/app_managed_auth_state__on_disk_cli_shared__oauth_user_authcode__with_browser.py b/examples/auth-session-management/app_managed_auth_state__on_disk_cli_shared__oauth_user_authcode__with_browser.py index 4beeb5a28..0067ffa28 100644 --- a/examples/auth-session-management/app_managed_auth_state__on_disk_cli_shared__oauth_user_authcode__with_browser.py +++ b/examples/auth-session-management/app_managed_auth_state__on_disk_cli_shared__oauth_user_authcode__with_browser.py @@ -18,10 +18,12 @@ def example_main(): save_state_to_storage=True, ) - # In contrast to an in-memory only application that must initialize a login every - # time, an app with persistent storage can skip this when it is not needed. - if not plsdk_auth.is_initialized(): - plsdk_auth.user_login(allow_open_browser=True) + # In contrast to in-memory applications that must initialize a login every + # time, an app with persistent storage can skip user prompts when they + # are not needed. + # This helper will prompt the user only when it is necessary. + plsdk_auth.ensure_initialized(allow_open_browser=True, + allow_tty_prompt=True) # Create a Planet SDK object that uses the loaded auth session. sess = planet.Session(plsdk_auth) diff --git a/examples/auth-session-management/app_managed_auth_state__on_disk_legacy__api_key.py b/examples/auth-session-management/app_managed_auth_state__on_disk_legacy__api_key.py index 7c0761069..572b7bb00 100644 --- a/examples/auth-session-management/app_managed_auth_state__on_disk_legacy__api_key.py +++ b/examples/auth-session-management/app_managed_auth_state__on_disk_legacy__api_key.py @@ -7,7 +7,7 @@ def example_main(): # specified file that was created with older versions of the SDK plsdk_auth = planet.Auth.from_file("legacy_api_key_file.json") - # Explicit login is not required for API key use. The above sufficient. + # Explicit login is not required for API key use. The above is sufficient. # plsdk_auth.user_login() # Create a Planet SDK object that uses the loaded auth session diff --git a/examples/auth-session-management/app_managed_auth_state__using_sdk_app_id.py b/examples/auth-session-management/app_managed_auth_state__using_sdk_app_id.py index afdc8fd96..63109fb23 100644 --- a/examples/auth-session-management/app_managed_auth_state__using_sdk_app_id.py +++ b/examples/auth-session-management/app_managed_auth_state__using_sdk_app_id.py @@ -10,10 +10,10 @@ def example_main(): # is false, the state will only be persistent in memory and the # user will need to login each time the application is run. plsdk_auth = planet.Auth.from_profile("planet-user", - save_state_to_storage=False) + save_state_to_storage=True) - if not plsdk_auth.is_initialized(): - plsdk_auth.user_login(allow_open_browser=True, allow_tty_prompt=True) + plsdk_auth.ensure_initialized(allow_open_browser=True, + allow_tty_prompt=True) # Create a Planet SDK object that uses the loaded auth session. sess = planet.Session(plsdk_auth) diff --git a/examples/auth-session-management/cli_managed_auth_state__explicit.py b/examples/auth-session-management/cli_managed_auth_state__explicit.py index 2ad7b8c42..deb2328e8 100644 --- a/examples/auth-session-management/cli_managed_auth_state__explicit.py +++ b/examples/auth-session-management/cli_managed_auth_state__explicit.py @@ -15,6 +15,10 @@ def example_main(): ) sys.exit(99) + # Alternatively, an application can call this, which will attempt to + # initialize the session if it is not already initialized. + # plsdk_auth.ensure_initialized(allow_open_browser=True, allow_tty_prompt=True) + # Create a Planet SDK object that uses the loaded auth session. sess = planet.Session(plsdk_auth) pl = planet.Planet(sess) diff --git a/planet/auth.py b/planet/auth.py index 688f0d64a..07149c634 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -21,7 +21,6 @@ import typing import warnings import httpx -from typing import List from .auth_builtins import _ProductionEnv, _OIDC_AUTH_CLIENT_CONFIG__USER_SKEL, _OIDC_AUTH_CLIENT_CONFIG__M2M_SKEL import planet_auth @@ -132,7 +131,7 @@ def from_profile( def from_oauth_user_auth_code( client_id: str, callback_url: str, - requested_scopes: typing.Optional[List[str]] = None, + requested_scopes: typing.Optional[typing.List[str]] = None, save_state_to_storage: bool = True, profile_name: typing.Optional[str] = None, storage_provider: typing.Optional[ @@ -193,7 +192,7 @@ def from_oauth_user_auth_code( @staticmethod def from_oauth_user_device_code( client_id: str, - requested_scopes: typing.Optional[List[str]] = None, + requested_scopes: typing.Optional[typing.List[str]] = None, save_state_to_storage: bool = True, profile_name: typing.Optional[str] = None, storage_provider: typing.Optional[ @@ -255,7 +254,7 @@ def from_oauth_user_device_code( def from_oauth_m2m( client_id: str, client_secret: str, - requested_scopes: typing.Optional[List[str]] = None, + requested_scopes: typing.Optional[typing.List[str]] = None, save_state_to_storage: bool = True, profile_name: typing.Optional[str] = None, storage_provider: typing.Optional[ @@ -456,7 +455,66 @@ def is_initialized(self) -> bool: user based sessions, this means that a login has been performed or saved login session data has been located. For M2M and API Key sessions, this should be true if keys or secrets have been - properly configured. + properly configured. The network will not be probed, and the user + will not be prompted by this method. + + Expired sessions or invalid credentials will not be detected. + See `ensure_initialized()` for a method that will check the validity + of sessions. + """ + + @abc.abstractmethod + def ensure_initialized( + self, + allow_open_browser: typing.Optional[bool] = False, + allow_tty_prompt: typing.Optional[bool] = False, + ) -> None: + """ + Do everything necessary to ensure the auth context is ready for use, + while still biasing towards just-in-time operations and not making + unnecessary network requests or prompts for user interaction. + + This can be more complex than it sounds given the variations in + possible session state. Clients may be initialized with active + sessions, initialized with stale but still valid sessions, + initialized with invalid or expired sessions, or completely + uninitialized. The process taken to ensure client readiness with + as little user disruption as possible is as follows: + + 1. If the client has been logged in and has a non-expired + session, the client will be considered ready without prompting + the user or probing the network. This will not require user + interaction. + 2. If the client has not been logged in and is an M2M client, + the client will be considered ready without prompting + the user or probing the network. This will not require + user interaction. Login will be delayed until it is required. + 3. If the client has been logged in and has an expired access token, + the network will be probed to attempt a refresh of the session. + This should not require user interaction. If refresh fails, + the user will be prompted to perform a fresh login, requiring + user interaction. + 4. If the client has never been logged in and is a user interactive + client (verses an M2M client), a user interactive login will be + initiated. + + There still may be conditions where we believe we are + ready, but requests will still ultimately fail. Saved secrets for M2M + clients could be wrong, or the user could be denied by API access + rules that are independent of session authentication. + + When a user interactive login is required, the client must specify + whether a local web browser may be opened and/or whether the TTY + may be used to prompt the user. What is appropriate will depend + on the nature of the application using the Planet SDK. + + If the auth context cannot be made ready, an exception will be raised. + + Parameters: + allow_open_browser: specify whether login is permitted to open + a local browser window. + allow_tty_prompt: specify whether login is permitted to request + input from the terminal. """ @@ -496,5 +554,15 @@ def device_user_login_complete(self, login_initialization_info: dict): def is_initialized(self) -> bool: return self._plauth.request_authenticator_is_ready() + def ensure_initialized( + self, + allow_open_browser: typing.Optional[bool] = False, + allow_tty_prompt: typing.Optional[bool] = False, + ) -> None: + return self._plauth.ensure_request_authenticator_is_ready( + allow_open_browser=allow_open_browser, + allow_tty_prompt=allow_tty_prompt, + ) + AuthType = Auth diff --git a/pyproject.toml b/pyproject.toml index 5627772fb..04837eca5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "pyjwt>=2.1", "tqdm>=4.56", "typing-extensions", - "planet-auth>=2.1.0", + "planet-auth>=2.3.0", ] readme = "README.md" requires-python = ">=3.10"