diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index b4abf4463..aa4fba0f9 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -77,6 +77,7 @@
+
@@ -107,5 +108,6 @@
+
\ No newline at end of file
diff --git a/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs b/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs
index 1c1402a8b..13c553667 100644
--- a/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs
+++ b/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs
@@ -21,6 +21,7 @@ namespace Bit.App.Pages
InitializeComponent();
_vm = BindingContext as LoginSsoPageViewModel;
_vm.Page = this;
+ _vm.FromIosExtension = _appOptions?.IosExtension ?? false;
_vm.StartTwoFactorAction = () => MainThread.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.StartSetPasswordAction = () =>
MainThread.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
diff --git a/src/Core/Pages/Accounts/LoginSsoPageViewModel.cs b/src/Core/Pages/Accounts/LoginSsoPageViewModel.cs
index 03945f2fd..d5b858063 100644
--- a/src/Core/Pages/Accounts/LoginSsoPageViewModel.cs
+++ b/src/Core/Pages/Accounts/LoginSsoPageViewModel.cs
@@ -15,6 +15,16 @@ using Bit.Core.Utilities;
using Microsoft.Maui.Authentication;
using Microsoft.Maui.Networking;
using NetworkAccess = Microsoft.Maui.Networking.NetworkAccess;
+using Org.BouncyCastle.Asn1.Ocsp;
+
+#if IOS
+using AuthenticationServices;
+using Foundation;
+using UIKit;
+using WebAuthenticator = Bit.Core.Utilities.MAUI.WebAuthenticator;
+using WebAuthenticatorResult = Bit.Core.Utilities.MAUI.WebAuthenticatorResult;
+using WebAuthenticatorOptions = Bit.Core.Utilities.MAUI.WebAuthenticatorOptions;
+#endif
namespace Bit.App.Pages
{
@@ -64,6 +74,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _orgIdentifier, value);
}
+ public bool FromIosExtension { get; set; }
+
public ICommand LogInCommand { get; }
public Action StartTwoFactorAction { get; set; }
public Action StartSetPasswordAction { get; set; }
@@ -153,6 +165,9 @@ namespace Bit.App.Pages
CallbackUrl = new Uri(REDIRECT_URI),
Url = new Uri(url),
PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession,
+#if IOS
+ ShouldUseSharedApplicationKeyWindow = FromIosExtension
+#endif
});
var code = GetResultCode(authResult, state);
diff --git a/src/Core/Services/Logging/DebugLogger.cs b/src/Core/Services/Logging/DebugLogger.cs
index a52c1de5d..21ac179b9 100644
--- a/src/Core/Services/Logging/DebugLogger.cs
+++ b/src/Core/Services/Logging/DebugLogger.cs
@@ -1,10 +1,6 @@
#if !FDROID
-using System;
-using System.Collections.Generic;
using System.Diagnostics;
-using System.IO;
using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
using Bit.Core.Abstractions;
namespace Bit.Core.Services
@@ -55,3 +51,5 @@ namespace Bit.Core.Services
}
}
#endif
+
+
diff --git a/src/Core/Services/StateMigrationService.cs b/src/Core/Services/StateMigrationService.cs
index 56883ada2..ed566a4b9 100644
--- a/src/Core/Services/StateMigrationService.cs
+++ b/src/Core/Services/StateMigrationService.cs
@@ -849,16 +849,18 @@ namespace Bit.Core.Services
{
// account data
var state = await GetValueAsync(Storage.Prefs, V7Keys.StateKey);
-
- // Migrate environment data to use Regions
- foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null && a.Value?.Settings != null))
+ if (state != null)
{
- var urls = account.Value.Settings.EnvironmentUrls ?? Region.US.GetUrls();
- account.Value.Settings.Region = urls.Region;
- account.Value.Settings.EnvironmentUrls = urls.Region.GetUrls() ?? urls;
- }
+ // Migrate environment data to use Regions
+ foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null && a.Value?.Settings != null))
+ {
+ var urls = account.Value.Settings.EnvironmentUrls ?? Region.US.GetUrls();
+ account.Value.Settings.Region = urls.Region;
+ account.Value.Settings.EnvironmentUrls = urls.Region.GetUrls() ?? urls;
+ }
- await SetValueAsync(Storage.Prefs, Constants.StateKey, state);
+ await SetValueAsync(Storage.Prefs, Constants.StateKey, state);
+ }
// Update pre auth urls and region
var preAuthUrls = await GetValueAsync(Storage.Prefs, V7Keys.PreAuthEnvironmentUrlsKey) ?? Region.US.GetUrls();
diff --git a/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.ios.cs b/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.ios.cs
new file mode 100644
index 000000000..3014c6de1
--- /dev/null
+++ b/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.ios.cs
@@ -0,0 +1,244 @@
+// This is a copy from MAUI Essentials WebAuthenticator with a fix for getting UIWindow without Scenes.
+
+#if IOS
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using AuthenticationServices;
+using Foundation;
+using SafariServices;
+using ObjCRuntime;
+using UIKit;
+using WebKit;
+using Microsoft.Maui.Authentication;
+using Microsoft.Maui.ApplicationModel;
+using Bit.Core.Services;
+
+namespace Bit.Core.Utilities.MAUI
+{
+ partial class WebAuthenticatorImplementation : IWebAuthenticator, IPlatformWebAuthenticatorCallback
+ {
+#if IOS
+ const int asWebAuthenticationSessionErrorCodeCanceledLogin = 1;
+ const string asWebAuthenticationSessionErrorDomain = "com.apple.AuthenticationServices.WebAuthenticationSession";
+
+ const int sfAuthenticationErrorCanceledLogin = 1;
+ const string sfAuthenticationErrorDomain = "com.apple.SafariServices.Authentication";
+#endif
+
+ TaskCompletionSource tcsResponse;
+ UIViewController currentViewController;
+ Uri redirectUri;
+ WebAuthenticatorOptions currentOptions;
+
+#if IOS
+ ASWebAuthenticationSession was;
+ SFAuthenticationSession sf;
+#endif
+
+ public async Task AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions)
+ {
+ currentOptions = webAuthenticatorOptions;
+ var url = webAuthenticatorOptions?.Url;
+ var callbackUrl = webAuthenticatorOptions?.CallbackUrl;
+ var prefersEphemeralWebBrowserSession = webAuthenticatorOptions?.PrefersEphemeralWebBrowserSession ?? false;
+
+ if (!VerifyHasUrlSchemeOrDoesntRequire(callbackUrl.Scheme))
+ throw new InvalidOperationException("You must register your URL Scheme handler in your app's Info.plist.");
+
+ // Cancel any previous task that's still pending
+ if (tcsResponse?.Task != null && !tcsResponse.Task.IsCompleted)
+ tcsResponse.TrySetCanceled();
+
+ tcsResponse = new TaskCompletionSource();
+ redirectUri = callbackUrl;
+ var scheme = redirectUri.Scheme;
+
+#if IOS
+ void AuthSessionCallback(NSUrl cbUrl, NSError error)
+ {
+ if (error == null)
+ OpenUrlCallback(cbUrl);
+ else if (error.Domain == asWebAuthenticationSessionErrorDomain && error.Code == asWebAuthenticationSessionErrorCodeCanceledLogin)
+ tcsResponse.TrySetCanceled();
+ else if (error.Domain == sfAuthenticationErrorDomain && error.Code == sfAuthenticationErrorCanceledLogin)
+ tcsResponse.TrySetCanceled();
+ else
+ tcsResponse.TrySetException(new NSErrorException(error));
+
+ was = null;
+ sf = null;
+ }
+
+ if (OperatingSystem.IsIOSVersionAtLeast(12))
+ {
+ was = new ASWebAuthenticationSession(MAUI.WebUtils.GetNativeUrl(url), scheme, AuthSessionCallback);
+
+ if (OperatingSystem.IsIOSVersionAtLeast(13))
+ {
+ var ctx = new ContextProvider(webAuthenticatorOptions.ShouldUseSharedApplicationKeyWindow
+ ? GetWorkaroundedUIWindow()
+ : WindowStateManager.Default.GetCurrentUIWindow());
+ was.PresentationContextProvider = ctx;
+ was.PrefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession;
+ }
+ else if (prefersEphemeralWebBrowserSession)
+ {
+ ClearCookies();
+ }
+
+ using (was)
+ {
+#pragma warning disable CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
+ was.Start();
+#pragma warning restore CA1416
+ return await tcsResponse.Task;
+ }
+ }
+
+ if (prefersEphemeralWebBrowserSession)
+ ClearCookies();
+
+#pragma warning disable CA1422 // 'SFAuthenticationSession' is obsoleted on: 'ios' 12.0 and later
+ if (OperatingSystem.IsIOSVersionAtLeast(11))
+ {
+ sf = new SFAuthenticationSession(MAUI.WebUtils.GetNativeUrl(url), scheme, AuthSessionCallback);
+ using (sf)
+ {
+ sf.Start();
+ return await tcsResponse.Task;
+ }
+ }
+#pragma warning restore CA1422
+
+ // This is only on iOS9+ but we only support 10+ in Essentials anyway
+ var controller = new SFSafariViewController(MAUI.WebUtils.GetNativeUrl(url), false)
+ {
+ Delegate = new NativeSFSafariViewControllerDelegate
+ {
+ DidFinishHandler = (svc) =>
+ {
+ // Cancel our task if it wasn't already marked as completed
+ if (!(tcsResponse?.Task?.IsCompleted ?? true))
+ tcsResponse.TrySetCanceled();
+ }
+ },
+ };
+
+ currentViewController = controller;
+ await WindowStateManager.Default.GetCurrentUIViewController().PresentViewControllerAsync(controller, true);
+#else
+ var opened = UIApplication.SharedApplication.OpenUrl(url);
+ if (!opened)
+ tcsResponse.TrySetException(new Exception("Error opening Safari"));
+#endif
+
+ return await tcsResponse.Task;
+ }
+
+ private UIWindow GetWorkaroundedUIWindow(bool throwIfNull = false)
+ {
+ var window = UIApplication.SharedApplication.KeyWindow;
+
+ if (window != null && window.WindowLevel == UIWindowLevel.Normal)
+ return window;
+
+ if (window == null)
+ {
+ window = UIApplication.SharedApplication
+ .Windows
+ .OrderByDescending(w => w.WindowLevel)
+ .FirstOrDefault(w => w.RootViewController != null && w.WindowLevel == UIWindowLevel.Normal);
+ }
+
+ if (throwIfNull && window == null)
+ throw new InvalidOperationException("Could not find current window.");
+
+ return window;
+
+ }
+
+ void ClearCookies()
+ {
+ NSUrlCache.SharedCache.RemoveAllCachedResponses();
+
+#if IOS
+ if (OperatingSystem.IsIOSVersionAtLeast(11))
+ {
+ WKWebsiteDataStore.DefaultDataStore.HttpCookieStore.GetAllCookies((cookies) =>
+ {
+ foreach (var cookie in cookies)
+ {
+#pragma warning disable CA1416 // Known false positive with lambda, here we can also assert the version
+ WKWebsiteDataStore.DefaultDataStore.HttpCookieStore.DeleteCookie(cookie, null);
+#pragma warning restore CA1416
+ }
+ });
+ }
+#endif
+ }
+
+ public bool OpenUrlCallback(Uri uri)
+ {
+ // If we aren't waiting on a task, don't handle the url
+ if (tcsResponse?.Task?.IsCompleted ?? true)
+ return false;
+
+ try
+ {
+ // If we can't handle the url, don't
+ if (!MAUI.WebUtils.CanHandleCallback(redirectUri, uri))
+ return false;
+
+ currentViewController?.DismissViewControllerAsync(true);
+ currentViewController = null;
+
+ tcsResponse.TrySetResult(new WebAuthenticatorResult(uri, currentOptions?.ResponseDecoder));
+ return true;
+ }
+ catch (Exception ex)
+ {
+ // TODO change this to ILogger?
+ Console.WriteLine(ex);
+ }
+ return false;
+ }
+
+ static bool VerifyHasUrlSchemeOrDoesntRequire(string scheme)
+ {
+ // app is currently supporting iOS11+ so no need for these checks.
+ return true;
+ //// iOS11+ uses sfAuthenticationSession which handles its own url routing
+ //if (OperatingSystem.IsIOSVersionAtLeast(11, 0) || OperatingSystem.IsTvOSVersionAtLeast(11, 0))
+ // return true;
+
+ //return AppInfoImplementation.VerifyHasUrlScheme(scheme);
+ }
+
+#if IOS
+ class NativeSFSafariViewControllerDelegate : SFSafariViewControllerDelegate
+ {
+ public Action DidFinishHandler { get; set; }
+
+ public override void DidFinish(SFSafariViewController controller) =>
+ DidFinishHandler?.Invoke(controller);
+ }
+
+ class ContextProvider : NSObject, IASWebAuthenticationPresentationContextProviding
+ {
+ public ContextProvider(UIWindow window) =>
+ Window = window;
+
+ public readonly UIWindow Window;
+
+ [Export("presentationAnchorForWebAuthenticationSession:")]
+ public UIWindow GetPresentationAnchor(ASWebAuthenticationSession session)
+ => Window;
+ }
+#endif
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.shared.cs b/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.shared.cs
new file mode 100644
index 000000000..c4bb1646e
--- /dev/null
+++ b/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.shared.cs
@@ -0,0 +1,188 @@
+// This is a copy from MAUI Essentials WebAuthenticator
+
+#if IOS
+
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Bit.Core.Utilities;
+
+namespace Bit.Core.Utilities.MAUI
+{
+ ///
+ /// A web navigation API intended to be used for authentication with external web services such as OAuth.
+ ///
+ public interface IWebAuthenticator
+ {
+ ///
+ /// Begin an authentication flow by navigating to the specified URL and waiting for a callback/redirect to the callback URL scheme.
+ ///
+ /// A instance containing additional configuration for this authentication call.
+ /// A object with the results of this operation.
+ Task AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions);
+ }
+
+ ///
+ /// Provides abstractions for the platform web authenticator callbacks triggered when using .
+ ///
+ public interface IPlatformWebAuthenticatorCallback
+ {
+#if IOS || MACCATALYST || MACOS
+ ///
+ /// Opens the specified URI to start the authentication flow.
+ ///
+ /// The URI to open that will start the authentication flow.
+ /// when the URI has been opened, otherwise .
+ bool OpenUrlCallback(Uri uri);
+#elif ANDROID
+ ///
+ /// The event that is triggered when an authentication flow calls back into the Android application.
+ ///
+ /// An object containing additional data about this resume operation.
+ /// when the callback can be processed, otherwise .
+ bool OnResumeCallback(Android.Content.Intent intent);
+#endif
+ }
+
+ ///
+ /// Provides abstractions used for decoding a URI returned from a authentication request, for use with .
+ ///
+ public interface IWebAuthenticatorResponseDecoder
+ {
+ ///
+ /// Decodes the given URIs query string into a dictionary.
+ ///
+ /// The object to decode the query parameters from.
+ /// A object where each of the query parameters values of are accessible through their respective keys.
+ IDictionary? DecodeResponse(Uri uri);
+ }
+
+ ///
+ /// A web navigation API intended to be used for Authentication with external web services such as OAuth.
+ ///
+ ///
+ /// This API helps with navigating to a start URL and waiting for a callback URL to the app. Your app must
+ /// be registered to handle the callback scheme you provide in the call to authenticate.
+ ///
+ public static class WebAuthenticator
+ {
+ /// Begin an authentication flow by navigating to the specified url and waiting for a callback/redirect to the callbackUrl scheme.
+ /// Url to navigate to, beginning the authentication flow.
+ /// Expected callback url that the navigation flow will eventually redirect to.
+ /// Returns a result parsed out from the callback url.
+ public static Task AuthenticateAsync(Uri url, Uri callbackUrl)
+ => Current.AuthenticateAsync(url, callbackUrl);
+
+ /// Begin an authentication flow by navigating to the specified url and waiting for a callback/redirect to the callbackUrl scheme.The start url and callbackUrl are specified in the webAuthenticatorOptions.
+ /// Options to configure the authentication request.
+ /// Returns a result parsed out from the callback url.
+ public static Task AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions)
+ => Current.AuthenticateAsync(webAuthenticatorOptions);
+
+ static IWebAuthenticator Current => Utilities.MAUI.WebAuthenticator.Default;
+
+ static IWebAuthenticator? defaultImplementation;
+
+ ///
+ /// Provides the default implementation for static usage of this API.
+ ///
+ public static IWebAuthenticator Default =>
+ defaultImplementation ??= new MAUI.WebAuthenticatorImplementation();
+
+ internal static void SetDefault(IWebAuthenticator? implementation) =>
+ defaultImplementation = implementation;
+ }
+
+ ///
+ /// This class contains static extension methods for use with .
+ ///
+ public static class WebAuthenticatorExtensions
+ {
+ static IPlatformWebAuthenticatorCallback AsPlatformCallback(this IWebAuthenticator webAuthenticator)
+ {
+ if (webAuthenticator is not IPlatformWebAuthenticatorCallback platform)
+ throw new PlatformNotSupportedException("This implementation of IWebAuthenticator does not implement IPlatformWebAuthenticatorCallback.");
+ return platform;
+ }
+
+#if ANDROID
+ internal static bool IsAuthenticatingWithCustomTabs(this IWebAuthenticator webAuthenticator)
+ => (webAuthenticator as MAUI.WebAuthenticatorImplementation)?.AuthenticatingWithCustomTabs ?? false;
+#endif
+
+ ///
+ /// Begin an authentication flow by navigating to the specified url and waiting for a callback/redirect to the callbackUrl scheme.
+ ///
+ /// The to use for the authentication flow.
+ /// Url to navigate to, beginning the authentication flow.
+ /// Expected callback url that the navigation flow will eventually redirect to.
+ /// Returns a result parsed out from the callback url.
+ public static Task AuthenticateAsync(this IWebAuthenticator webAuthenticator, Uri url, Uri callbackUrl) =>
+ webAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions { Url = url, CallbackUrl = callbackUrl });
+
+#if IOS || MACCATALYST || MACOS
+ ///
+ public static bool OpenUrl(this IWebAuthenticator webAuthenticator, Uri uri) =>
+ webAuthenticator.AsPlatformCallback().OpenUrlCallback(uri);
+
+ ///
+ public static bool OpenUrl(this IWebAuthenticator webAuthenticator, UIKit.UIApplication app, Foundation.NSUrl url, Foundation.NSDictionary options)
+ {
+ if(url?.AbsoluteString != null)
+ {
+ return webAuthenticator.OpenUrl(new Uri(url.AbsoluteString));
+ }
+ return false;
+ }
+
+ ///
+ public static bool ContinueUserActivity(this IWebAuthenticator webAuthenticator, UIKit.UIApplication application, Foundation.NSUserActivity userActivity, UIKit.UIApplicationRestorationHandler completionHandler)
+ {
+ var uri = userActivity?.WebPageUrl?.AbsoluteString;
+ if (string.IsNullOrEmpty(uri))
+ return false;
+
+ return webAuthenticator.OpenUrl(new Uri(uri));
+ }
+
+#elif ANDROID
+ ///
+ public static bool OnResume(this IWebAuthenticator webAuthenticator, Android.Content.Intent intent) =>
+ webAuthenticator.AsPlatformCallback().OnResumeCallback(intent);
+#endif
+ }
+
+ ///
+ /// Represents additional options for .
+ ///
+ public class WebAuthenticatorOptions
+ {
+ ///
+ /// Gets or sets the URL that will start the authentication flow.
+ ///
+ public Uri? Url { get; set; }
+
+ ///
+ /// Gets or sets the callback URL that should be called when authentication completes.
+ ///
+ public Uri? CallbackUrl { get; set; }
+
+ ///
+ /// Gets or sets whether the browser used for the authentication flow is short-lived.
+ /// This means it will not share session nor cookies with the regular browser on this device if set the .
+ ///
+ /// This setting only has effect on iOS.
+ public bool PrefersEphemeralWebBrowserSession { get; set; }
+
+ ///
+ /// Gets or sets the decoder implementation used to decode the incoming authentication result URI.
+ ///
+ public IWebAuthenticatorResponseDecoder? ResponseDecoder { get; set; }
+
+ public bool ShouldUseSharedApplicationKeyWindow { get; set; }
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticatorResult.cs b/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticatorResult.cs
new file mode 100644
index 000000000..818d5fee9
--- /dev/null
+++ b/src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticatorResult.cs
@@ -0,0 +1,152 @@
+// This is a copy from MAUI Essentials WebAuthenticator
+
+#if IOS
+
+using System;
+using System.Collections.Generic;
+using Bit.Core.Utilities.MAUI;
+using Microsoft.Maui.ApplicationModel;
+
+namespace Bit.Core.Utilities.MAUI
+{
+ ///
+ /// Represents a Web Authenticator Result object parsed from the callback Url.
+ ///
+ ///
+ /// All of the query string or url fragment properties are parsed into a dictionary and can be accessed by their key.
+ ///
+ public class WebAuthenticatorResult
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public WebAuthenticatorResult()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class by parsing a URI's query string parameters.
+ ///
+ /// The callback uri that was used to end the authentication sequence.
+ public WebAuthenticatorResult(Uri uri) : this(uri, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class by parsing a URI's query string parameters.
+ ///
+ ///
+ /// If the responseDecoder is non-null, then it is used to decode the fragment or query string
+ /// returned by the authorization service. Otherwise, a default response decoder is used.
+ ///
+ /// The callback uri that was used to end the authentication sequence.
+ /// The decoder that can be used to decode the callback uri.
+ public WebAuthenticatorResult(Uri uri, IWebAuthenticatorResponseDecoder responseDecoder)
+ {
+ CallbackUri = uri;
+ var properties = responseDecoder?.DecodeResponse(uri) ?? WebUtils.ParseQueryString(uri);
+ foreach (var kvp in properties)
+ {
+ Properties[kvp.Key] = kvp.Value;
+ }
+ }
+
+ ///
+ /// Create a new instance from an existing dictionary.
+ ///
+ /// The dictionary of properties to incorporate.
+ public WebAuthenticatorResult(IDictionary properties)
+ {
+ foreach (var kvp in properties)
+ Properties[kvp.Key] = kvp.Value;
+ }
+
+ ///
+ /// The uri that was used to call back with the access token.
+ ///
+ ///
+ /// The value of the callback URI, including the fragment or query string bearing
+ /// the access token and associated information.
+ ///
+ public Uri CallbackUri { get; }
+
+ ///
+ /// The timestamp when the class was instantiated, which usually corresponds with the parsed result of a request.
+ ///
+ public DateTimeOffset Timestamp { get; set; } = new DateTimeOffset(DateTime.UtcNow);
+
+ ///
+ /// The dictionary of key/value pairs parsed form the callback URI's query string.
+ ///
+ public Dictionary Properties { get; set; } = new(StringComparer.Ordinal);
+
+ /// Puts a key/value pair into the dictionary.
+ public void Put(string key, string value)
+ => Properties[key] = value;
+
+ /// Gets a value for a given key from the dictionary.
+ /// Key from the callback URI's query string.
+ public string Get(string key)
+ {
+ if (Properties.TryGetValue(key, out var v))
+ return v;
+
+ return default;
+ }
+
+ /// The value for the `access_token` key.
+ /// Access Token parsed from the callback URI access_token parameter.
+ public string AccessToken
+ => Get("access_token");
+
+ /// The value for the `refresh_token` key.
+ /// Refresh Token parsed from the callback URI refresh_token parameter.
+ public string RefreshToken
+ => Get("refresh_token");
+
+ /// The value for the `id_token` key.
+ /// The value for the `id_token` key.
+ /// Apple doesn't return an access token on iOS native sign in, but it does return id_token as a JWT.
+ public string IdToken
+ => Get("id_token");
+
+ ///
+ /// The refresh token expiry date as calculated by the timestamp of when the result was created plus
+ /// the value in seconds for the refresh_token_expires_in key.
+ ///
+ /// Timestamp of the creation of the object instance plus the expires_in seconds parsed from the callback URI.
+ public DateTimeOffset? RefreshTokenExpiresIn
+ {
+ get
+ {
+ if (Properties.TryGetValue("refresh_token_expires_in", out var v))
+ {
+ if (int.TryParse(v, out var i))
+ return Timestamp.AddSeconds(i);
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// The expiry date as calculated by the timestamp of when the result was created plus
+ /// the value in seconds for the `expires_in` key.
+ ///
+ /// Timestamp of the creation of the object instance plus the expires_in seconds parsed from the callback URI.
+ public DateTimeOffset? ExpiresIn
+ {
+ get
+ {
+ if (Properties.TryGetValue("expires_in", out var v))
+ {
+ if (int.TryParse(v, out var i))
+ return Timestamp.AddSeconds(i);
+ }
+
+ return null;
+ }
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Core/Utilities/WebAuthenticatorMAUI/WebUtils.cs b/src/Core/Utilities/WebAuthenticatorMAUI/WebUtils.cs
new file mode 100644
index 000000000..b9a6a4285
--- /dev/null
+++ b/src/Core/Utilities/WebAuthenticatorMAUI/WebUtils.cs
@@ -0,0 +1,126 @@
+// This is copied from MAUI repo to be used from WebAuthenticator
+// https://github.com/dotnet/maui/blob/main/src/Essentials/src/Types/Shared/WebUtils.shared.cs
+
+#if IOS
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Bit.Core.Utilities.MAUI
+{
+ static class WebUtils
+ {
+ internal static IDictionary ParseQueryString(Uri uri)
+ {
+ var parameters = new Dictionary(StringComparer.Ordinal);
+
+ if (uri == null)
+ return parameters;
+
+ // Note: Uri.Query starts with a '?'
+ if (!string.IsNullOrEmpty(uri.Query))
+ UnpackParameters(uri.Query.AsSpan(1), parameters);
+
+ // Note: Uri.Fragment starts with a '#'
+ if (!string.IsNullOrEmpty(uri.Fragment))
+ UnpackParameters(uri.Fragment.AsSpan(1), parameters);
+
+ return parameters;
+ }
+
+ // The following method is a port of the logic found in https://source.dot.net/#Microsoft.AspNetCore.WebUtilities/src/Shared/QueryStringEnumerable.cs
+ // but refactored such that it:
+ //
+ // 1. avoids the IEnumerable overhead that isn't needed (the ASP.NET logic was clearly designed that way to offer a public API whereas we don't need that)
+ // 2. avoids the use of unsafe code
+ static void UnpackParameters(ReadOnlySpan query, Dictionary parameters)
+ {
+ while (!query.IsEmpty)
+ {
+ int delimeterIndex = query.IndexOf('&');
+ ReadOnlySpan segment;
+
+ if (delimeterIndex >= 0)
+ {
+ segment = query.Slice(0, delimeterIndex);
+ query = query.Slice(delimeterIndex + 1);
+ }
+ else
+ {
+ segment = query;
+ query = default;
+ }
+
+ // If it's nonempty, emit it
+ if (!segment.IsEmpty)
+ {
+ var equalIndex = segment.IndexOf('=');
+ string name, value;
+
+ if (equalIndex >= 0)
+ {
+ name = segment.Slice(0, equalIndex).ToString();
+
+ var span = segment.Slice(equalIndex + 1);
+ var chars = new char[span.Length];
+
+ for (int i = 0; i < span.Length; i++)
+ chars[i] = span[i] == '+' ? ' ' : span[i];
+
+ value = new string(chars);
+ }
+ else
+ {
+ name = segment.ToString();
+ value = string.Empty;
+ }
+
+ name = Uri.UnescapeDataString(name);
+
+ parameters[name] = Uri.UnescapeDataString(value);
+ }
+ }
+ }
+
+ internal static Uri EscapeUri(Uri uri)
+ {
+ if (uri == null)
+ throw new ArgumentNullException(nameof(uri));
+
+ var idn = new global::System.Globalization.IdnMapping();
+ return new Uri(uri.Scheme + "://" + idn.GetAscii(uri.Authority) + uri.PathAndQuery + uri.Fragment);
+ }
+
+ internal static bool CanHandleCallback(Uri expectedUrl, Uri callbackUrl)
+ {
+ if (!callbackUrl.Scheme.Equals(expectedUrl.Scheme, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ if (!string.IsNullOrEmpty(expectedUrl.Host))
+ {
+ if (!callbackUrl.Host.Equals(expectedUrl.Host, StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
+
+ return true;
+ }
+
+#if __IOS__ || __TVOS__ || __MACOS__
+ internal static Foundation.NSUrl GetNativeUrl(Uri uri)
+ {
+ try
+ {
+ return new Foundation.NSUrl(uri.OriginalString);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Unable to create NSUrl from Original string, trying Absolute URI: {ex.Message}");
+ return new Foundation.NSUrl(uri.AbsoluteUri);
+ }
+ }
+#endif
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs
index 2ad523d22..57593fec1 100644
--- a/src/iOS.Autofill/CredentialProviderViewController.cs
+++ b/src/iOS.Autofill/CredentialProviderViewController.cs
@@ -55,7 +55,6 @@ namespace Bit.iOS.Autofill
{
ExtContext = ExtensionContext
};
-
}
catch (Exception ex)
{
@@ -156,6 +155,7 @@ namespace Bit.iOS.Autofill
{
InitAppIfNeeded();
_context.Configuring = true;
+
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
@@ -522,8 +522,9 @@ namespace Bit.iOS.Autofill
private void LaunchLoginSsoFlow()
{
- var loginPage = new LoginSsoPage();
- var app = new App.App(new AppOptions { IosExtension = true });
+ var appOptions = new AppOptions { IosExtension = true };
+ var loginPage = new LoginSsoPage(appOptions);
+ var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
diff --git a/src/iOS.Autofill/LockPasswordViewController.cs b/src/iOS.Autofill/LockPasswordViewController.cs
index 0d73e1fff..9455754a1 100644
--- a/src/iOS.Autofill/LockPasswordViewController.cs
+++ b/src/iOS.Autofill/LockPasswordViewController.cs
@@ -1,8 +1,8 @@
using System;
using Bit.App.Controls;
+using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.iOS.Core.Utilities;
-using MapKit;
using UIKit;
namespace Bit.iOS.Autofill
@@ -33,22 +33,29 @@ namespace Bit.iOS.Autofill
public override async void ViewDidLoad()
{
- _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
-
- base.ViewDidLoad();
-
- _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
-
- _accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
- _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
-
- NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
+ try
{
- _cancelButton,
- new UIBarButtonItem(_accountSwitchButton)
- }, false);
+ _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
- _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
+ base.ViewDidLoad();
+
+ _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
+
+ _accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
+ _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
+
+ NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
+ {
+ _cancelButton,
+ new UIBarButtonItem(_accountSwitchButton)
+ }, false);
+
+ _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.LogEvenIfCantBeResolved(ex);
+ }
}
private void CancelButton_TouchUpInside(object sender, EventArgs e)
diff --git a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs
index e01502558..31477f403 100644
--- a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs
+++ b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs
@@ -16,6 +16,7 @@ using Bit.iOS.Core.Views;
using Foundation;
using UIKit;
using Microsoft.Maui.Controls.Compatibility;
+using Microsoft.Maui.Platform;
namespace Bit.iOS.Core.Controllers
{
@@ -222,20 +223,27 @@ namespace Bit.iOS.Core.Controllers
public override void ViewDidAppear(bool animated)
{
- base.ViewDidAppear(animated);
-
- // Users with key connector and without biometric or pin has no MP to unlock with
- if (!_hasMasterPassword)
+ try
{
- if (!(_pinEnabled || _biometricEnabled) ||
- (_biometricEnabled && !_biometricIntegrityValid))
+ base.ViewDidAppear(animated);
+
+ // Users with key connector and without biometric or pin has no MP to unlock with
+ if (!_hasMasterPassword)
{
- PromptSSO();
+ if (!(_pinEnabled || _biometricEnabled) ||
+ (_biometricEnabled && !_biometricIntegrityValid))
+ {
+ PromptSSO();
+ }
+ }
+ else if (!_biometricEnabled || !_biometricIntegrityValid)
+ {
+ MasterPasswordCell.TextField.BecomeFirstResponder();
}
}
- else if (!_biometricEnabled || !_biometricIntegrityValid)
+ catch (Exception ex)
{
- MasterPasswordCell.TextField.BecomeFirstResponder();
+ LoggerHelper.LogEvenIfCantBeResolved(ex);
}
}
@@ -433,8 +441,9 @@ namespace Bit.iOS.Core.Controllers
public void PromptSSO()
{
- var loginPage = new LoginSsoPage();
- var app = new App.App(new AppOptions { IosExtension = true });
+ var appOptions = new AppOptions { IosExtension = true };
+ var loginPage = new LoginSsoPage(appOptions);
+ var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
@@ -444,7 +453,7 @@ namespace Bit.iOS.Core.Controllers
}
var navigationPage = new NavigationPage(loginPage);
- var loginController = navigationPage.CreateViewController();
+ var loginController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext);
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
diff --git a/src/iOS.Core/Controllers/LoginAddViewController.cs b/src/iOS.Core/Controllers/LoginAddViewController.cs
index 385655fc1..2081b07d2 100644
--- a/src/iOS.Core/Controllers/LoginAddViewController.cs
+++ b/src/iOS.Core/Controllers/LoginAddViewController.cs
@@ -18,6 +18,7 @@ using Bit.iOS.Core.Views;
using Foundation;
using UIKit;
using Microsoft.Maui.Controls.Compatibility;
+using Microsoft.Maui.Platform;
namespace Bit.iOS.Core.Controllers
{
@@ -239,7 +240,7 @@ namespace Bit.iOS.Core.Controllers
ThemeManager.ApplyResourcesTo(generatorPage);
var navigationPage = new NavigationPage(generatorPage);
- var generatorController = navigationPage.CreateViewController();
+ var generatorController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext);
generatorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(generatorController, true, null);
}
diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs
index 75d8cc8a6..570bec9dd 100644
--- a/src/iOS.Extension/LoadingViewController.cs
+++ b/src/iOS.Extension/LoadingViewController.cs
@@ -599,8 +599,9 @@ namespace Bit.iOS.Extension
private void LaunchLoginSsoFlow()
{
- var loginPage = new LoginSsoPage();
- var app = new App.App(new AppOptions { IosExtension = true });
+ var appOptions = new AppOptions { IosExtension = true };
+ var loginPage = new LoginSsoPage(appOptions);
+ var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs
index acf295d56..d97150487 100644
--- a/src/iOS.ShareExtension/LoadingViewController.cs
+++ b/src/iOS.ShareExtension/LoadingViewController.cs
@@ -381,7 +381,7 @@ namespace Bit.iOS.ShareExtension
private void LaunchLoginSsoFlow()
{
- var loginPage = new LoginSsoPage();
+ var loginPage = new LoginSsoPage(_appOptions.Value);
SetupAppAndApplyResources(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
{