[PM-6539] Fix Autofill Extension TDE without MP flow (#3049)

* PM-6539 Fix Autofill Extension TDE without MP updating PromptSSO to work in MAUI and also Generator view. WebAuthenticator copied with UIWindow gotten as it was in Xamarin forms to work. Also fix one NRE on state migration.

* PM-6539 Remove unnecessary using
This commit is contained in:
Federico Maccaroni 2024-03-05 18:09:20 -03:00 committed by GitHub
parent 6f6487ccc9
commit 19f238d9bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 793 additions and 46 deletions

View File

@ -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>

View File

@ -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());

View File

@ -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);

View File

@ -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

View File

@ -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();

View 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

View File

@ -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

View File

@ -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

View 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

View File

@ -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)

View File

@ -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)

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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)

View File

@ -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)
{