mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
[PM-6539] Fix Autofill Extension TDE without MP flow (#3049)
This commit is contained in:
@@ -77,6 +77,7 @@
|
||||
<Folder Include="Resources\Localization\" />
|
||||
<Folder Include="Controls\Picker\" />
|
||||
<Folder Include="Controls\Avatar\" />
|
||||
<Folder Include="Utilities\WebAuthenticatorMAUI\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<MauiImage Include="Resources\Images\dotnet_bot.svg">
|
||||
@@ -107,5 +108,6 @@
|
||||
<ItemGroup>
|
||||
<None Remove="Controls\Picker\" />
|
||||
<None Remove="Controls\Avatar\" />
|
||||
<None Remove="Utilities\WebAuthenticatorMAUI\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -849,16 +849,18 @@ namespace Bit.Core.Services
|
||||
{
|
||||
// account data
|
||||
var state = await GetValueAsync<State>(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<EnvironmentUrlData>(Storage.Prefs, V7Keys.PreAuthEnvironmentUrlsKey) ?? Region.US.GetUrls();
|
||||
|
||||
244
src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.ios.cs
Normal file
244
src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.ios.cs
Normal file
@@ -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<WebAuthenticatorResult> tcsResponse;
|
||||
UIViewController currentViewController;
|
||||
Uri redirectUri;
|
||||
WebAuthenticatorOptions currentOptions;
|
||||
|
||||
#if IOS
|
||||
ASWebAuthenticationSession was;
|
||||
SFAuthenticationSession sf;
|
||||
#endif
|
||||
|
||||
public async Task<WebAuthenticatorResult> 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<WebAuthenticatorResult>();
|
||||
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<SFSafariViewController> 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
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A web navigation API intended to be used for authentication with external web services such as OAuth.
|
||||
/// </summary>
|
||||
public interface IWebAuthenticator
|
||||
{
|
||||
/// <summary>
|
||||
/// Begin an authentication flow by navigating to the specified URL and waiting for a callback/redirect to the callback URL scheme.
|
||||
/// </summary>
|
||||
/// <param name="webAuthenticatorOptions">A <see cref="WebAuthenticatorOptions"/> instance containing additional configuration for this authentication call.</param>
|
||||
/// <returns>A <see cref="WebAuthenticatorResult"/> object with the results of this operation.</returns>
|
||||
Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides abstractions for the platform web authenticator callbacks triggered when using <see cref="WebAuthenticator"/>.
|
||||
/// </summary>
|
||||
public interface IPlatformWebAuthenticatorCallback
|
||||
{
|
||||
#if IOS || MACCATALYST || MACOS
|
||||
/// <summary>
|
||||
/// Opens the specified URI to start the authentication flow.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to open that will start the authentication flow.</param>
|
||||
/// <returns><see langword="true"/> when the URI has been opened, otherwise <see langword="false"/>.</returns>
|
||||
bool OpenUrlCallback(Uri uri);
|
||||
#elif ANDROID
|
||||
/// <summary>
|
||||
/// The event that is triggered when an authentication flow calls back into the Android application.
|
||||
/// </summary>
|
||||
/// <param name="intent">An <see cref="Android.Content.Intent"/> object containing additional data about this resume operation.</param>
|
||||
/// <returns><see langword="true"/> when the callback can be processed, otherwise <see langword="false"/>.</returns>
|
||||
bool OnResumeCallback(Android.Content.Intent intent);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides abstractions used for decoding a URI returned from a authentication request, for use with <see cref="IWebAuthenticator"/>.
|
||||
/// </summary>
|
||||
public interface IWebAuthenticatorResponseDecoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Decodes the given URIs query string into a dictionary.
|
||||
/// </summary>
|
||||
/// <param name="uri">The <see cref="Uri"/> object to decode the query parameters from.</param>
|
||||
/// <returns>A <see cref="IDictionary{TKey, TValue}"/> object where each of the query parameters values of <paramref name="uri"/> are accessible through their respective keys.</returns>
|
||||
IDictionary<string, string>? DecodeResponse(Uri uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A web navigation API intended to be used for Authentication with external web services such as OAuth.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public static class WebAuthenticator
|
||||
{
|
||||
/// <summary>Begin an authentication flow by navigating to the specified url and waiting for a callback/redirect to the callbackUrl scheme.</summary>
|
||||
/// <param name="url"> Url to navigate to, beginning the authentication flow.</param>
|
||||
/// <param name="callbackUrl"> Expected callback url that the navigation flow will eventually redirect to.</param>
|
||||
/// <returns>Returns a result parsed out from the callback url.</returns>
|
||||
public static Task<WebAuthenticatorResult> AuthenticateAsync(Uri url, Uri callbackUrl)
|
||||
=> Current.AuthenticateAsync(url, callbackUrl);
|
||||
|
||||
/// <summary>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.</summary>
|
||||
/// <param name="webAuthenticatorOptions">Options to configure the authentication request.</param>
|
||||
/// <returns>Returns a result parsed out from the callback url.</returns>
|
||||
public static Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions)
|
||||
=> Current.AuthenticateAsync(webAuthenticatorOptions);
|
||||
|
||||
static IWebAuthenticator Current => Utilities.MAUI.WebAuthenticator.Default;
|
||||
|
||||
static IWebAuthenticator? defaultImplementation;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the default implementation for static usage of this API.
|
||||
/// </summary>
|
||||
public static IWebAuthenticator Default =>
|
||||
defaultImplementation ??= new MAUI.WebAuthenticatorImplementation();
|
||||
|
||||
internal static void SetDefault(IWebAuthenticator? implementation) =>
|
||||
defaultImplementation = implementation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class contains static extension methods for use with <see cref="WebAuthenticator"/>.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Begin an authentication flow by navigating to the specified url and waiting for a callback/redirect to the callbackUrl scheme.
|
||||
/// </summary>
|
||||
/// <param name="webAuthenticator">The <see cref="IWebAuthenticator"/> to use for the authentication flow.</param>
|
||||
/// <param name="url"> Url to navigate to, beginning the authentication flow.</param>
|
||||
/// <param name="callbackUrl"> Expected callback url that the navigation flow will eventually redirect to.</param>
|
||||
/// <returns>Returns a result parsed out from the callback url.</returns>
|
||||
public static Task<WebAuthenticatorResult> AuthenticateAsync(this IWebAuthenticator webAuthenticator, Uri url, Uri callbackUrl) =>
|
||||
webAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions { Url = url, CallbackUrl = callbackUrl });
|
||||
|
||||
#if IOS || MACCATALYST || MACOS
|
||||
/// <inheritdoc cref="IPlatformWebAuthenticatorCallback.OpenUrlCallback(Uri)"/>
|
||||
public static bool OpenUrl(this IWebAuthenticator webAuthenticator, Uri uri) =>
|
||||
webAuthenticator.AsPlatformCallback().OpenUrlCallback(uri);
|
||||
|
||||
/// <inheritdoc cref="ApplicationModel.Platform.OpenUrl(UIKit.UIApplication, Foundation.NSUrl, Foundation.NSDictionary)"/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ApplicationModel.Platform.ContinueUserActivity(UIKit.UIApplication, Foundation.NSUserActivity, UIKit.UIApplicationRestorationHandler)"/>
|
||||
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
|
||||
/// <inheritdoc cref="IPlatformWebAuthenticatorCallback.OnResumeCallback(Android.Content.Intent)"/>
|
||||
public static bool OnResume(this IWebAuthenticator webAuthenticator, Android.Content.Intent intent) =>
|
||||
webAuthenticator.AsPlatformCallback().OnResumeCallback(intent);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents additional options for <see cref="WebAuthenticator"/>.
|
||||
/// </summary>
|
||||
public class WebAuthenticatorOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URL that will start the authentication flow.
|
||||
/// </summary>
|
||||
public Uri? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback URL that should be called when authentication completes.
|
||||
/// </summary>
|
||||
public Uri? CallbackUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see langword="true"/>.
|
||||
/// </summary>
|
||||
/// <remarks>This setting only has effect on iOS.</remarks>
|
||||
public bool PrefersEphemeralWebBrowserSession { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the decoder implementation used to decode the incoming authentication result URI.
|
||||
/// </summary>
|
||||
public IWebAuthenticatorResponseDecoder? ResponseDecoder { get; set; }
|
||||
|
||||
public bool ShouldUseSharedApplicationKeyWindow { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Web Authenticator Result object parsed from the callback Url.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All of the query string or url fragment properties are parsed into a dictionary and can be accessed by their key.
|
||||
/// </remarks>
|
||||
public class WebAuthenticatorResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebAuthenticatorResult"/> class.
|
||||
/// </summary>
|
||||
public WebAuthenticatorResult()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebAuthenticatorResult"/> class by parsing a URI's query string parameters.
|
||||
/// </summary>
|
||||
/// <param name="uri">The callback uri that was used to end the authentication sequence.</param>
|
||||
public WebAuthenticatorResult(Uri uri) : this(uri, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebAuthenticatorResult"/> class by parsing a URI's query string parameters.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="uri">The callback uri that was used to end the authentication sequence.</param>
|
||||
/// <param name="responseDecoder">The decoder that can be used to decode the callback uri.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance from an existing dictionary.
|
||||
/// </summary>
|
||||
/// <param name="properties">The dictionary of properties to incorporate.</param>
|
||||
public WebAuthenticatorResult(IDictionary<string, string> properties)
|
||||
{
|
||||
foreach (var kvp in properties)
|
||||
Properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The uri that was used to call back with the access token.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The value of the callback URI, including the fragment or query string bearing
|
||||
/// the access token and associated information.
|
||||
/// </value>
|
||||
public Uri CallbackUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp when the class was instantiated, which usually corresponds with the parsed result of a request.
|
||||
/// </summary>
|
||||
public DateTimeOffset Timestamp { get; set; } = new DateTimeOffset(DateTime.UtcNow);
|
||||
|
||||
/// <summary>
|
||||
/// The dictionary of key/value pairs parsed form the callback URI's query string.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Properties { get; set; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Puts a key/value pair into the dictionary.</summary>
|
||||
public void Put(string key, string value)
|
||||
=> Properties[key] = value;
|
||||
|
||||
/// <summary>Gets a value for a given key from the dictionary.</summary>
|
||||
/// <param name="key">Key from the callback URI's query string.</param>
|
||||
public string Get(string key)
|
||||
{
|
||||
if (Properties.TryGetValue(key, out var v))
|
||||
return v;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>The value for the `access_token` key.</summary>
|
||||
/// <value>Access Token parsed from the callback URI access_token parameter.</value>
|
||||
public string AccessToken
|
||||
=> Get("access_token");
|
||||
|
||||
/// <summary>The value for the `refresh_token` key.</summary>
|
||||
/// <value>Refresh Token parsed from the callback URI refresh_token parameter.</value>
|
||||
public string RefreshToken
|
||||
=> Get("refresh_token");
|
||||
|
||||
/// <summary>The value for the `id_token` key.</summary>
|
||||
/// <value>The value for the `id_token` key.</value>
|
||||
/// <remarks>Apple doesn't return an access token on iOS native sign in, but it does return id_token as a JWT.</remarks>
|
||||
public string IdToken
|
||||
=> Get("id_token");
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>Timestamp of the creation of the object instance plus the expires_in seconds parsed from the callback URI.</value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The expiry date as calculated by the timestamp of when the result was created plus
|
||||
/// the value in seconds for the `expires_in` key.
|
||||
/// </summary>
|
||||
/// <value>Timestamp of the creation of the object instance plus the expires_in seconds parsed from the callback URI.</value>
|
||||
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
|
||||
126
src/Core/Utilities/WebAuthenticatorMAUI/WebUtils.cs
Normal file
126
src/Core/Utilities/WebAuthenticatorMAUI/WebUtils.cs
Normal file
@@ -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<string, string> ParseQueryString(Uri uri)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>(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<char> query, Dictionary<string, string> parameters)
|
||||
{
|
||||
while (!query.IsEmpty)
|
||||
{
|
||||
int delimeterIndex = query.IndexOf('&');
|
||||
ReadOnlySpan<char> 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user