From 7684e0f90af17df93c19e62a5f8145769daf8030 Mon Sep 17 00:00:00 2001 From: isra-fel <11371776+isra-fel@users.noreply.github.com> Date: Fri, 22 May 2026 21:33:09 +1000 Subject: [PATCH 1/2] broker sso --- src/azure-cli-core/azure/cli/core/_profile.py | 6 +++++- .../azure/cli/core/auth/identity.py | 20 ++++++++++++++++--- .../cli/command_modules/profile/__init__.py | 2 ++ .../cli/command_modules/profile/custom.py | 12 ++++++++++- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index de0c8efe70f..e9ccd9e4f06 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -148,6 +148,7 @@ def login(self, is_service_principal, tenant, scopes=None, + use_broker_sso=False, use_device_code=False, allow_no_subscriptions=False, use_cert_sn_issuer=None, @@ -176,7 +177,10 @@ def login(self, if use_device_code: user_identity = identity.login_with_device_code(scopes=scopes, claims_challenge=claims_challenge) else: - user_identity = identity.login_with_auth_code(scopes=scopes, claims_challenge=claims_challenge) + user_identity = identity.login_with_auth_code( + scopes=scopes, + claims_challenge=claims_challenge, + use_broker_sso=use_broker_sso) else: if not is_service_principal: user_identity = identity.login_with_username_password(username, password, scopes=scopes) diff --git a/src/azure-cli-core/azure/cli/core/auth/identity.py b/src/azure-cli-core/azure/cli/core/auth/identity.py index 91629e89441..8ae9e04e45b 100644 --- a/src/azure-cli-core/azure/cli/core/auth/identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/identity.py @@ -145,7 +145,7 @@ def _service_principal_store(self): Identity._service_principal_store_instance = ServicePrincipalStore(store) return Identity._service_principal_store_instance - def login_with_auth_code(self, scopes, claims_challenge=None): + def login_with_auth_code(self, scopes, claims_challenge=None, use_broker_sso=False): # Emit a warning to inform that a browser is opened. # Only show the path part of the URL and hide the query string. @@ -164,12 +164,18 @@ def _prompt_launching_ui(ui=None, **_): # For AAD, use port 0 to let the system choose arbitrary unused ephemeral port to avoid port collision # on port 8400 from the old design. However, ADFS only allows port 8400. result = self._msal_app.acquire_token_interactive( - scopes, prompt='select_account', port=8400 if self._is_adfs else None, + scopes, + prompt='none' if use_broker_sso else 'select_account', + port=8400 if self._is_adfs else None, success_template=success_template, error_template=error_template, parent_window_handle=self._msal_app.CONSOLE_WINDOW_HANDLE, on_before_launching_ui=_prompt_launching_ui, enable_msa_passthrough=True, claims_challenge=claims_challenge) - return check_result(result) + parsed = check_result(result) + if use_broker_sso: + # log parsed result in debug level + logger.debug("Result from broker SSO login: %s", json.dumps(parsed, indent=4)) + return parsed def login_with_device_code(self, scopes, claims_challenge=None): flow = self._msal_app.initiate_device_flow(scopes, claims_challenge=claims_challenge) @@ -244,6 +250,14 @@ def get_service_principal_credential(self, client_id): client_credential = ServicePrincipalAuth(entry).get_msal_client_credential() return ServicePrincipalCredential(client_id, client_credential, **self._msal_app_kwargs) + # checks are now done in custom.py + # def _is_broker_sso(self): + # """Detect if SSO with broker is possible. + # It is only possible if broker is available (Windows for now) and enabled via config, + # AND user has opted in with the flag. + # """ + # return self._enable_broker_on_windows and sys.platform.startswith('win') and self._broker_sso + class ServicePrincipalAuth: # pylint: disable=too-many-instance-attributes def __init__(self, entry): diff --git a/src/azure-cli/azure/cli/command_modules/profile/__init__.py b/src/azure-cli/azure/cli/command_modules/profile/__init__.py index af25643541d..4fa4bc0d786 100644 --- a/src/azure-cli/azure/cli/command_modules/profile/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/profile/__init__.py @@ -58,6 +58,8 @@ def load_arguments(self, command): c.argument('claims_challenge', help="Base64-encoded claims challenge requested by a resource API in the " "WWW-Authenticate header.") + c.argument('use_broker_sso', action='store_true', + help="(TBD) Enable broker SSO for login.") c.ignore('_subscription') # hide the global subscription parameter # Skip subscription discovery diff --git a/src/azure-cli/azure/cli/command_modules/profile/custom.py b/src/azure-cli/azure/cli/command_modules/profile/custom.py index 164a362a054..e8b6b639af6 100644 --- a/src/azure-cli/azure/cli/command_modules/profile/custom.py +++ b/src/azure-cli/azure/cli/command_modules/profile/custom.py @@ -134,7 +134,7 @@ def _select_and_set_active(profile, subscriptions): # pylint: disable=too-many-branches, too-many-locals def login(cmd, username=None, password=None, tenant=None, scopes=None, allow_no_subscriptions=False, - claims_challenge=None, + claims_challenge=None, use_broker_sso=False, # Device code flow use_device_code=False, # Service principal @@ -169,6 +169,15 @@ def login(cmd, username=None, password=None, tenant=None, scopes=None, allow_no_ logger.warning(USERNAME_PASSWORD_DEPRECATION_WARNING_AZURE_CLOUD) else: logger.warning(USERNAME_PASSWORD_DEPRECATION_WARNING_OTHER_CLOUD) + # todo: broker_sso incompatible with workload identities + # broker_sso relies on the presence of a broker, which is only available on Windows (for now) + enable_broker_on_windows = cmd.cli_ctx.config.getboolean('core', 'enable_broker_on_windows', fallback=True) + broker_sso_available = sys.platform.startswith('win') and enable_broker_on_windows + if use_broker_sso and not broker_sso_available: + raise CLIError("usage error: '--use-broker-sso' is only supported on Windows with broker enabled") + if use_broker_sso and (service_principal or identity): + raise CLIError("usage error: '--use-broker-sso' is not applicable " + "to service principal or managed identity login") if claims_challenge: from azure.cli.core.util import b64decode @@ -219,6 +228,7 @@ def login(cmd, username=None, password=None, tenant=None, scopes=None, allow_no_ service_principal, tenant, scopes=scopes, + use_broker_sso=use_broker_sso, use_device_code=use_device_code, allow_no_subscriptions=allow_no_subscriptions, use_cert_sn_issuer=use_cert_sn_issuer, From 7ede217aba476e65c2965510d2bbe84d5d6ef0a9 Mon Sep 17 00:00:00 2001 From: isra-fel <11371776+isra-fel@users.noreply.github.com> Date: Mon, 25 May 2026 12:05:43 +1000 Subject: [PATCH 2/2] broker sso when agent mode --- src/azure-cli-core/azure/cli/core/auth/agentic_session.py | 3 +++ src/azure-cli-core/azure/cli/core/auth/identity.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/auth/agentic_session.py b/src/azure-cli-core/azure/cli/core/auth/agentic_session.py index 33112322331..9b195335a56 100644 --- a/src/azure-cli-core/azure/cli/core/auth/agentic_session.py +++ b/src/azure-cli-core/azure/cli/core/auth/agentic_session.py @@ -25,6 +25,9 @@ COPILOT_AGENT_SESSION_ID = "COPILOT_AGENT_SESSION_ID" +def is_agentic_session(): + """Determine if we're in an agentic session based on the presence of COPILOT_AGENT_SESSION_ID.""" + return os.environ.get(COPILOT_AGENT_SESSION_ID) is not None def build_agentic_session_params(): """Read COPILOT_AGENT_SESSION_ID and build the agentic claims challenge. diff --git a/src/azure-cli-core/azure/cli/core/auth/identity.py b/src/azure-cli-core/azure/cli/core/auth/identity.py index 8ae9e04e45b..e46e5130405 100644 --- a/src/azure-cli-core/azure/cli/core/auth/identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/identity.py @@ -161,11 +161,14 @@ def _prompt_launching_ui(ui=None, **_): from .util import read_response_templates success_template, error_template = read_response_templates() + from .agentic_session import is_agentic_session + prompt = 'none' if use_broker_sso or is_agentic_session() else 'select_account' + # For AAD, use port 0 to let the system choose arbitrary unused ephemeral port to avoid port collision # on port 8400 from the old design. However, ADFS only allows port 8400. result = self._msal_app.acquire_token_interactive( scopes, - prompt='none' if use_broker_sso else 'select_account', + prompt=prompt, port=8400 if self._is_adfs else None, success_template=success_template, error_template=error_template, parent_window_handle=self._msal_app.CONSOLE_WINDOW_HANDLE, on_before_launching_ui=_prompt_launching_ui,