From fbf1e2dfd2cf2c6cc490d2e83d5dbc821cb4b96f Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Mon, 14 Sep 2026 09:07:37 +0200 Subject: [PATCH 1/5] register MSAL broker app queries and improve error messages --- src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs | 10 +++++++--- .../Manifests/AndroidManifest_net.xml | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs index c5edf6096..07a127a23 100644 --- a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs @@ -1020,9 +1020,12 @@ public async void OnStart(IFileStorageSetupActivity activity) } catch (Exception e) { - logDebug("authenticating not successful: " + e); + string errorMessage = "authenticating not successful"; + if (e is MsalException msalEx) + errorMessage += " (" + msalEx.ErrorCode + ")"; + logDebug(errorMessage + ": " + e); Intent data = new Intent(); - data.PutExtra(FileStorageSetupDefs.ExtraErrorMessage, "authenticating not successful"); + data.PutExtra(FileStorageSetupDefs.ExtraErrorMessage, errorMessage); ((Activity)activity).SetResult(Result.Canceled, data); ((Activity)activity).Finish(); } @@ -1086,7 +1089,8 @@ private async Task TryLoginSilent(string iocPath) } catch (Exception ex) { - logDebug("silent login failed: " + ex.ToString()); + string? errorCode = (ex as MsalException)?.ErrorCode; + logDebug("silent login failed" + (errorCode != null ? " (" + errorCode + ")" : "") + ": " + ex); return null; } } diff --git a/src/keepass2android-app/Manifests/AndroidManifest_net.xml b/src/keepass2android-app/Manifests/AndroidManifest_net.xml index 81159fa58..b77a5f24d 100644 --- a/src/keepass2android-app/Manifests/AndroidManifest_net.xml +++ b/src/keepass2android-app/Manifests/AndroidManifest_net.xml @@ -32,6 +32,11 @@ + + + + + From abddc33266d146ba17d0eec583964902c144d278 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Mon, 14 Sep 2026 09:21:02 +0200 Subject: [PATCH 2/5] fallback to non-broker auth in case of error --- .../Io/OneDrive2FileStorage.cs | 94 +++++++++++++++++-- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs index 07a127a23..43acefcc3 100644 --- a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs @@ -17,6 +17,7 @@ using System.Reflection; using System.Text; using Android.Content; +using Android.Preferences; using Android.Util; using KeePass.Util; using keepass2android.Io.ItemLocation; @@ -198,6 +199,24 @@ public OneDrive2ItemLocation BuildShare(string id, public static IPublicClientApplication _publicClientApp = null; private string ClientID = "8374f801-0f55-407d-80cc-9a04fe86d9b2"; + // MSAL broker (WAM/Authenticator/Company Portal) is required to pass the device ID for + // Intune/Entra-managed devices, but broken broker setups (missing package visibility, + // missing broker redirect URI registration, ...) must not block ordinary OneDrive sign-in. + // We try broker first and permanently fall back to non-broker for this device once we see + // a broker-specific failure. + private const string PrefKeyBrokerAvailable = "OneDrive2FileStorageBrokerAvailable"; + + private static readonly HashSet BrokerFailureErrorCodes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "cannot_invoke_broker", + "no_broker_installed_on_device", + "no_broker_account_found", + "null_intent_returned_from_broker", + "broker_signature_verification_failed", + "android_broker_operation_failed", + }; + + private static bool _brokerEnabledForCurrentClient; public abstract IEnumerable Scopes { @@ -206,10 +225,51 @@ public abstract IEnumerable Scopes public OneDrive2FileStorage() { - _publicClientApp = PublicClientApplicationBuilder.Create(ClientID) - .WithRedirectUri($"msal{ClientID}://auth") - .WithBroker(true) // Enable broker authentication to pass device ID for Intune/Entra managed devices - .Build(); + _brokerEnabledForCurrentClient = IsBrokerAllowedByCache(); + _publicClientApp = BuildPublicClientApp(_brokerEnabledForCurrentClient); + } + + private IPublicClientApplication BuildPublicClientApp(bool useBroker) + { + var builder = PublicClientApplicationBuilder.Create(ClientID) + .WithRedirectUri($"msal{ClientID}://auth"); + if (useBroker) + builder = builder.WithBroker(true); + return builder.Build(); + } + + private bool IsBrokerAllowedByCache() + { + try + { + return PreferenceManager.GetDefaultSharedPreferences(Application.Context) + .GetBoolean(PrefKeyBrokerAvailable, true); + } + catch (Exception e) + { + logDebug("failed to read broker-available preference: " + e); + return true; + } + } + + private static bool IsBrokerFailure(MsalException ex) + { + return ex.ErrorCode != null && BrokerFailureErrorCodes.Contains(ex.ErrorCode); + } + + private void DisableBrokerAndRebuildClient() + { + _brokerEnabledForCurrentClient = false; + _publicClientApp = BuildPublicClientApp(false); + try + { + PreferenceManager.GetDefaultSharedPreferences(Application.Context) + .Edit().PutBoolean(PrefKeyBrokerAvailable, false).Commit(); + } + catch (Exception e) + { + logDebug("failed to persist broker-unavailable preference: " + e); + } } class PathItemBuilder @@ -1009,9 +1069,21 @@ public async void OnStart(IFileStorageSetupActivity activity) { logDebug("try interactive"); - AuthenticationResult res = await _publicClientApp.AcquireTokenInteractive(Scopes) - .WithParentActivityOrWindow((Activity)activity) - .ExecuteAsync(); + AuthenticationResult res; + try + { + res = await _publicClientApp.AcquireTokenInteractive(Scopes) + .WithParentActivityOrWindow((Activity)activity) + .ExecuteAsync(); + } + catch (MsalException msalEx) when (_brokerEnabledForCurrentClient && IsBrokerFailure(msalEx)) + { + logDebug("interactive auth failed with broker error " + msalEx.ErrorCode + ", retrying without broker"); + DisableBrokerAndRebuildClient(); + res = await _publicClientApp.AcquireTokenInteractive(Scopes) + .WithParentActivityOrWindow((Activity)activity) + .ExecuteAsync(); + } logDebug("ok interactive"); BuildClient(res); FinishActivityWithSuccess(activity, BuildRootPathForUser(res)); @@ -1087,6 +1159,14 @@ private async Task TryLoginSilent(string iocPath) logDebug("ui required"); return null; } + catch (MsalException msalEx) when (_brokerEnabledForCurrentClient && IsBrokerFailure(msalEx)) + { + // The broker-backed account/token cache is not usable once broker itself is broken. + // Disable broker for future logins and fall back to a fresh interactive sign-in. + logDebug("silent auth failed with broker error " + msalEx.ErrorCode + ", disabling broker for future logins"); + DisableBrokerAndRebuildClient(); + return null; + } catch (Exception ex) { string? errorCode = (ex as MsalException)?.ErrorCode; From fa4cd656727941950b330af259964200b119adc5 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Mon, 14 Sep 2026 09:21:14 +0200 Subject: [PATCH 3/5] fix potential crash --- .../Io/OneDrive2FileStorage.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs index 43acefcc3..92a64b9d7 100644 --- a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs @@ -1053,20 +1053,19 @@ protected void FinishActivityWithSuccess( public async void OnStart(IFileStorageSetupActivity activity) { - logDebug("OneDrive2.OnStart"); - if (activity.ProcessName.Equals(FileStorageSetupDefs.ProcessNameFileUsageSetup)) - activity.State.PutString(FileStorageSetupDefs.ExtraPath, activity.Ioc.Path); - string rootPathForUser = await TryLoginSilent(activity.Ioc.Path); - if (rootPathForUser != null) - { - logDebug("rootPathForUser not null"); - FinishActivityWithSuccess(activity, rootPathForUser); - return; - } - logDebug("rootPathForUser null"); - try { + logDebug("OneDrive2.OnStart"); + if (activity.ProcessName.Equals(FileStorageSetupDefs.ProcessNameFileUsageSetup)) + activity.State.PutString(FileStorageSetupDefs.ExtraPath, activity.Ioc.Path); + string rootPathForUser = await TryLoginSilent(activity.Ioc.Path); + if (rootPathForUser != null) + { + logDebug("rootPathForUser not null"); + FinishActivityWithSuccess(activity, rootPathForUser); + return; + } + logDebug("rootPathForUser null"); logDebug("try interactive"); AuthenticationResult res; @@ -1087,8 +1086,6 @@ public async void OnStart(IFileStorageSetupActivity activity) logDebug("ok interactive"); BuildClient(res); FinishActivityWithSuccess(activity, BuildRootPathForUser(res)); - - } catch (Exception e) { From 7b5ae47aa07f3d121c69ae2d2ee169c62b15e85a Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Mon, 14 Sep 2026 09:49:11 +0200 Subject: [PATCH 4/5] add preferences to choose broker behavior --- .../Io/OneDrive2FileStorage.cs | 65 +++++++++++++++++-- .../Resources/values/config.xml | 8 +++ .../Resources/values/strings.xml | 7 ++ .../Resources/xml/pref_app_file_handling.xml | 9 +++ 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs index 92a64b9d7..0b6677479 100644 --- a/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs @@ -202,8 +202,26 @@ public OneDrive2ItemLocation BuildShare(string id, // MSAL broker (WAM/Authenticator/Company Portal) is required to pass the device ID for // Intune/Entra-managed devices, but broken broker setups (missing package visibility, // missing broker redirect URI registration, ...) must not block ordinary OneDrive sign-in. - // We try broker first and permanently fall back to non-broker for this device once we see - // a broker-specific failure. + // The user-facing "OneDrive device sign-in" preference (pref_app_file_handling.xml) picks + // between: Auto (try broker, permanently fall back to non-broker for this device on a + // broker-specific failure), Always (force broker, surface real errors, no fallback - + // for admins who need to confirm/require the Intune device-ID path), and Never (skip + // broker entirely - the escape hatch for devices where broker itself is broken). + private enum BrokerMode + { + Auto, + Always, + Never + } + + // Must match Resource.String.OneDriveBrokerMode_key's value (config.xml) - this business-logic + // assembly has no access to the app's generated Resource class, so the key is duplicated here. + private const string PrefKeyBrokerMode = "OneDriveBrokerMode"; + private const string BrokerModeValueAlways = "ALWAYS"; + private const string BrokerModeValueNever = "NEVER"; + + // Sticky per-device flag learned in Auto mode once broker is seen to fail; independent of + // PrefKeyBrokerMode so switching back to Auto later remembers the outcome. private const string PrefKeyBrokerAvailable = "OneDrive2FileStorageBrokerAvailable"; private static readonly HashSet BrokerFailureErrorCodes = new HashSet(StringComparer.OrdinalIgnoreCase) @@ -216,6 +234,7 @@ public OneDrive2ItemLocation BuildShare(string id, "android_broker_operation_failed", }; + private static BrokerMode _brokerMode; private static bool _brokerEnabledForCurrentClient; public abstract IEnumerable Scopes @@ -225,7 +244,13 @@ public abstract IEnumerable Scopes public OneDrive2FileStorage() { - _brokerEnabledForCurrentClient = IsBrokerAllowedByCache(); + _brokerMode = ReadBrokerModePreference(); + _brokerEnabledForCurrentClient = _brokerMode switch + { + BrokerMode.Always => true, + BrokerMode.Never => false, + _ => IsBrokerAllowedByCache(), + }; _publicClientApp = BuildPublicClientApp(_brokerEnabledForCurrentClient); } @@ -238,11 +263,30 @@ private IPublicClientApplication BuildPublicClientApp(bool useBroker) return builder.Build(); } + private BrokerMode ReadBrokerModePreference() + { + try + { + string value = PreferenceManager.GetDefaultSharedPreferences(Android.App.Application.Context) + .GetString(PrefKeyBrokerMode, null); + if (value == BrokerModeValueAlways) + return BrokerMode.Always; + if (value == BrokerModeValueNever) + return BrokerMode.Never; + return BrokerMode.Auto; + } + catch (Exception e) + { + logDebug("failed to read broker-mode preference: " + e); + return BrokerMode.Auto; + } + } + private bool IsBrokerAllowedByCache() { try { - return PreferenceManager.GetDefaultSharedPreferences(Application.Context) + return PreferenceManager.GetDefaultSharedPreferences(Android.App.Application.Context) .GetBoolean(PrefKeyBrokerAvailable, true); } catch (Exception e) @@ -257,13 +301,20 @@ private static bool IsBrokerFailure(MsalException ex) return ex.ErrorCode != null && BrokerFailureErrorCodes.Contains(ex.ErrorCode); } + // Only Auto mode auto-recovers from a broker failure by falling back to non-broker; Always + // mode intentionally surfaces the real error instead of silently masking a broken broker. + private static bool ShouldFallBackFromBroker(MsalException ex) + { + return _brokerMode == BrokerMode.Auto && _brokerEnabledForCurrentClient && IsBrokerFailure(ex); + } + private void DisableBrokerAndRebuildClient() { _brokerEnabledForCurrentClient = false; _publicClientApp = BuildPublicClientApp(false); try { - PreferenceManager.GetDefaultSharedPreferences(Application.Context) + PreferenceManager.GetDefaultSharedPreferences(Android.App.Application.Context) .Edit().PutBoolean(PrefKeyBrokerAvailable, false).Commit(); } catch (Exception e) @@ -1075,7 +1126,7 @@ public async void OnStart(IFileStorageSetupActivity activity) .WithParentActivityOrWindow((Activity)activity) .ExecuteAsync(); } - catch (MsalException msalEx) when (_brokerEnabledForCurrentClient && IsBrokerFailure(msalEx)) + catch (MsalException msalEx) when (ShouldFallBackFromBroker(msalEx)) { logDebug("interactive auth failed with broker error " + msalEx.ErrorCode + ", retrying without broker"); DisableBrokerAndRebuildClient(); @@ -1156,7 +1207,7 @@ private async Task TryLoginSilent(string iocPath) logDebug("ui required"); return null; } - catch (MsalException msalEx) when (_brokerEnabledForCurrentClient && IsBrokerFailure(msalEx)) + catch (MsalException msalEx) when (ShouldFallBackFromBroker(msalEx)) { // The broker-backed account/token cache is not usable once broker itself is broken. // Disable broker for future logins and fall back to a fresh interactive sign-in. diff --git a/src/keepass2android-app/Resources/values/config.xml b/src/keepass2android-app/Resources/values/config.xml index b84fee880..9e41e5456 100644 --- a/src/keepass2android-app/Resources/values/config.xml +++ b/src/keepass2android-app/Resources/values/config.xml @@ -213,6 +213,14 @@ ERROR + OneDriveBrokerMode + AUTO + + AUTO + ALWAYS + NEVER + + ShowUnlockedNotification true 65536 diff --git a/src/keepass2android-app/Resources/values/strings.xml b/src/keepass2android-app/Resources/values/strings.xml index f583fd3f4..5b8f20f38 100644 --- a/src/keepass2android-app/Resources/values/strings.xml +++ b/src/keepass2android-app/Resources/values/strings.xml @@ -415,6 +415,8 @@ Show local backups SSL certificates Define the behavior when certificate validation fails. Note: you can install certificates on your device if validation fails! + OneDrive device sign-in (broker) + Controls whether OneDrive sign-in uses your device\'s Microsoft account manager (broker), which is required on some Intune/Entra-managed work devices. Automatic tries broker sign-in first and falls back to normal sign-in if it fails. Choose "Never" if OneDrive sign-in fails repeatedly on this device. Clear cache? This will delete all cached database files. Any changes you made while having no access to the source database and which have not yet been synchronized will be lost! Continue? Check for modifications @@ -1282,6 +1284,11 @@ Warn when validation fails Do not accept invalid certificates + + Automatic (recommended) + Always use device sign-in + Never use device sign-in + Make sure this works on your system and consider using the built-in keyboard if not. Description provided by the plugin: Keepass2Android supports Android\'s Autofill feature but it looks like you haven\'t enabled it yet. diff --git a/src/keepass2android-app/Resources/xml/pref_app_file_handling.xml b/src/keepass2android-app/Resources/xml/pref_app_file_handling.xml index e232ef512..3cd14594f 100644 --- a/src/keepass2android-app/Resources/xml/pref_app_file_handling.xml +++ b/src/keepass2android-app/Resources/xml/pref_app_file_handling.xml @@ -70,6 +70,15 @@ android:dialogTitle="@string/AcceptAllServerCertificates_title" android:defaultValue="@string/AcceptAllServerCertificates_default" /> + + Date: Mon, 14 Sep 2026 10:31:19 +0200 Subject: [PATCH 5/5] add broker packages in debug manifest as well for testing --- src/keepass2android-app/Manifests/AndroidManifest_debug.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/keepass2android-app/Manifests/AndroidManifest_debug.xml b/src/keepass2android-app/Manifests/AndroidManifest_debug.xml index 2045646d2..8e085de54 100644 --- a/src/keepass2android-app/Manifests/AndroidManifest_debug.xml +++ b/src/keepass2android-app/Manifests/AndroidManifest_debug.xml @@ -31,6 +31,11 @@ + + + + +