1
0
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:
Federico Maccaroni
2024-03-05 18:13:57 -03:00
parent 7f92358d9b
commit ad308c97c9
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)
{