From 944eaf11f88d4ed211e787774e61dde182704b0d Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Thu, 19 Sep 2024 14:34:03 +0200 Subject: [PATCH 1/2] fix(workaround): wait for broker DBus path to become visible The broker implements DBus activation. However, it appears on the bus before it can fully be used (i.e. before the /com/microsoft/identity/broker1 becomes visible). By that, the buffered API calls are dispatched too early and return with an UnknownObject error. This affects both the introspection and API calls. To work around this, we poll for the path to show up before performing any other broker communication. Once the broker disappears, we clear our internal state to ensure the polling happens again before the next invocation. This logic makes it possible to run the broker only when needed and also solves issues around the initial startup of the broker via the web extension. Signed-off-by: Felix Moessbauer --- linux-entra-sso.py | 97 +++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/linux-entra-sso.py b/linux-entra-sso.py index f34dbe8..80ceab1 100755 --- a/linux-entra-sso.py +++ b/linux-entra-sso.py @@ -13,9 +13,10 @@ import struct import uuid import ctypes +import time from signal import SIGINT from threading import Thread, Lock -from gi.repository import GLib +from gi.repository import GLib, Gio from pydbus import SessionBus # version is replaced on installation @@ -27,6 +28,7 @@ # value can be used, if no real value is provided. SSO_URL_DEFAULT = "https://login.microsoftonline.com/" EDGE_BROWSER_CLIENT_ID = "d7b530a4-7680-4c23-a8bf-c52c121d2e87" +BROKER_START_TIMEOUT = 5 # dbus start service reply codes START_REPLY_SUCCESS = 1 START_REPLY_ALREADY_RUNNING = 2 @@ -68,7 +70,6 @@ def send_message(encoded_message): class SsoMib: - NO_BROKER = {'error': 'Broker not available'} BROKER_NAME = 'com.microsoft.identity.broker1' BROKER_PATH = '/com/microsoft/identity/broker1' GRAPH_SCOPES = ["https://graph.microsoft.com/.default"] @@ -76,25 +77,28 @@ class SsoMib: def __init__(self, daemon=False): self._bus = SessionBus() self.broker = None - self.broker_online = False self.session_id = uuid.uuid4() self._state_changed_cb = None - self._check_broker_online() if daemon: + self._introspect_broker(fail_on_error=False) self._monitor_bus() - def _check_broker_online(self): - dbus = self._bus.get('org.freedesktop.DBus', '/org/freedesktop/DBus') - if dbus.NameHasOwner(self.BROKER_NAME) \ - or dbus.StartServiceByName(self.BROKER_NAME, 0) in \ - [START_REPLY_ALREADY_RUNNING, START_REPLY_SUCCESS]: - self._instantiate_broker() - self.broker_online = True - else: - self.broker_online = False - - def _instantiate_broker(self): - self.broker = self._bus.get(self.BROKER_NAME, self.BROKER_PATH) + def _introspect_broker(self, fail_on_error=True): + timeout = time.time() + BROKER_START_TIMEOUT + while not self.broker and time.time() < timeout: + try: + self.broker = self._bus.get(self.BROKER_NAME, self.BROKER_PATH) + return + except GLib.Error as err: + # GDBus.Error:org.freedesktop.dbus.errors.UnknownObject: + # Introspecting on non-existant object + # See https://github.com/siemens/linux-entra-sso/issues/33 + if err.matches(Gio.io_error_quark(), + Gio.IOErrorEnum.DBUS_ERROR): + time.sleep(0.1) + continue + if fail_on_error: + raise RuntimeError("Could not start broker") def _monitor_bus(self): self._bus.subscribe( @@ -108,13 +112,15 @@ def _broker_state_changed(self, sender, object, iface, signal, params): \ # pylint: disable=redefined-builtin,too-many-arguments _ = (sender, object, iface, signal) # params = (name, old_owner, new_owner) - if params[2]: - self._instantiate_broker() - self.broker_online = True + new_owner = params[2] + if new_owner: + self._introspect_broker() else: - self.broker_online = False + # we need to ensure that the next dbus call will + # wait until the broker is fully initialized again + self.broker = None if self._state_changed_cb: - self._state_changed_cb(self.broker_online) + self._state_changed_cb(new_owner) def on_broker_state_changed(self, callback): """ @@ -139,8 +145,7 @@ def _get_auth_parameters(account, scopes): } def get_accounts(self): - if not self.broker_online: - return self.NO_BROKER + self._introspect_broker() context = { 'clientId': EDGE_BROWSER_CLIENT_ID, 'redirectUri': str(self.session_id) @@ -152,8 +157,7 @@ def get_accounts(self): def acquire_prt_sso_cookie(self, account, sso_url, scopes=GRAPH_SCOPES): \ # pylint: disable=dangerous-default-value - if not self.broker_online: - return self.NO_BROKER + self._introspect_broker() request = { 'account': account, 'authParameters': SsoMib._get_auth_parameters(account, scopes), @@ -165,8 +169,7 @@ def acquire_prt_sso_cookie(self, account, sso_url, scopes=GRAPH_SCOPES): \ def acquire_token_silently(self, account, scopes=GRAPH_SCOPES): \ # pylint: disable=dangerous-default-value - if not self.broker_online: - return self.NO_BROKER + self._introspect_broker() request = { 'account': account, 'authParameters': SsoMib._get_auth_parameters(account, scopes), @@ -176,8 +179,7 @@ def acquire_token_silently(self, account, scopes=GRAPH_SCOPES): \ return token def get_broker_version(self): - if not self.broker_online: - return self.NO_BROKER + self._introspect_broker() params = json.dumps({"msalCppVersion": LINUX_ENTRA_SSO_VERSION}) resp = json.loads( self.broker.getLinuxBrokerVersion('0.0', @@ -189,6 +191,7 @@ def get_broker_version(self): def run_as_native_messaging(): iomutex = Lock() + no_broker = {'error': 'Broker not available'} def respond(command, message): NativeMessaging.send_message( @@ -199,9 +202,25 @@ def notify_state_change(online): with iomutex: respond("brokerStateChanged", 'online' if online else 'offline') + def handle_command(cmd, received_message): + if cmd == "acquirePrtSsoCookie": + account = received_message['account'] + sso_url = received_message['ssoUrl'] or SSO_URL_DEFAULT + token = ssomib.acquire_prt_sso_cookie(account, sso_url) + respond(cmd, token) + elif cmd == "acquireTokenSilently": + account = received_message['account'] + scopes = received_message.get('scopes') or ssomib.GRAPH_SCOPES + token = ssomib.acquire_token_silently(account, scopes) + respond(cmd, token) + elif cmd == "getAccounts": + respond(cmd, ssomib.get_accounts()) + elif cmd == "getVersion": + respond(cmd, ssomib.get_broker_version()) + def run_dbus_monitor(): # inform other side about initial state - notify_state_change(ssomib.broker_online) + notify_state_change(bool(ssomib.broker)) loop = GLib.MainLoop() loop.run() @@ -224,20 +243,10 @@ def register_terminate_with_parent(): received_message = NativeMessaging.get_message() with iomutex: cmd = received_message['command'] - if cmd == "acquirePrtSsoCookie": - account = received_message['account'] - sso_url = received_message['ssoUrl'] or SSO_URL_DEFAULT - token = ssomib.acquire_prt_sso_cookie(account, sso_url) - respond(cmd, token) - elif cmd == "acquireTokenSilently": - account = received_message['account'] - scopes = received_message.get('scopes') or ssomib.GRAPH_SCOPES - token = ssomib.acquire_token_silently(account, scopes) - respond(cmd, token) - elif cmd == "getAccounts": - respond(cmd, ssomib.get_accounts()) - elif cmd == "getVersion": - respond(cmd, ssomib.get_broker_version()) + try: + handle_command(cmd, received_message) + except Exception: # pylint: disable=broad-except + respond(cmd, no_broker) def run_interactive(): From 7b79d9a36c531825add9152d68a3206a12ac1981 Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Thu, 19 Sep 2024 16:59:05 +0200 Subject: [PATCH 2/2] feat: remain logged in during broker outages Previously, we precisely tracked the broker state and logged the user out in case the broker disappeared. This however made it impossible to auto-disable the broker after some time and only re-enable it when needed again. Now, we keep the user logged in once the frontend communicated once with the broker. When needed again, we just call into the backend and let it handle the startup, waiting and errors. Signed-off-by: Felix Moessbauer --- background.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/background.js b/background.js index ac1c807..1b8e343 100644 --- a/background.js +++ b/background.js @@ -51,7 +51,7 @@ async function waitFor(f) { * Check if all conditions for SSO are met */ function is_operational() { - return state_active && accounts.active && broker_online + return state_active && accounts.active } /* @@ -327,12 +327,14 @@ async function on_message_native(response) { if (response.message == 'online') { ssoLog('connection to broker restored'); broker_online = true; - await load_accounts(); - port_native.postMessage({'command': 'getVersion'}); + // only reload data if we did not see the broker before + if (host_versions.native === null) { + await load_accounts(); + port_native.postMessage({'command': 'getVersion'}); + } } else { ssoLog('lost connection to broker'); broker_online = false; - logout(); } } else {