[Android] [WebAuth] Rework authentication flow (#1511)

- The 2FA process breaks the flow but opening a separate activity from a separate app. This results in the flow being interpreted as a cancellation.
- The new flow uses an intermediate activity to correctly process the results.
- Also converted the BindService into an async method.
- Fixes #1292
This commit is contained in:
Matthew Leibowitz 2020-11-11 12:41:47 +02:00 коммит произвёл GitHub
Родитель a8b189887c
Коммит 4d5941a10f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 135 добавлений и 65 удалений

Просмотреть файл

@ -106,8 +106,6 @@ namespace Xamarin.Essentials
{
if (activity != null)
CheckAppActions(activity.Intent);
WebAuthenticator.OnResume(null);
}
static void CheckAppActions(AndroidIntent intent)

Просмотреть файл

@ -12,15 +12,7 @@ namespace Xamarin.Essentials
public partial class WebAuthenticator
{
static TaskCompletionSource<WebAuthenticatorResult> tcsResponse = null;
static Uri uri = null;
static CustomTabsActivityManager CustomTabsActivityManager { get; set; }
static Uri RedirectUri { get; set; }
internal static Task<WebAuthenticatorResult> ResponseTask
=> tcsResponse?.Task;
static Uri currentRedirectUri = null;
internal static bool OnResume(Intent intent)
{
@ -28,7 +20,7 @@ namespace Xamarin.Essentials
if (tcsResponse?.Task?.IsCompleted ?? true)
return false;
if (intent == null)
if (intent?.Data == null)
{
tcsResponse.TrySetCanceled();
return false;
@ -39,9 +31,9 @@ namespace Xamarin.Essentials
var intentUri = new Uri(intent.Data.ToString());
// Only handle schemes we expect
if (!WebUtils.CanHandleCallback(RedirectUri, intentUri))
if (!WebUtils.CanHandleCallback(currentRedirectUri, intentUri))
{
tcsResponse.TrySetException(new InvalidOperationException($"Invalid Redirect URI, detected `{intentUri}` but expected a URI in the format of `{RedirectUri}`"));
tcsResponse.TrySetException(new InvalidOperationException($"Invalid Redirect URI, detected `{intentUri}` but expected a URI in the format of `{currentRedirectUri}`"));
return false;
}
@ -55,7 +47,7 @@ namespace Xamarin.Essentials
}
}
static Task<WebAuthenticatorResult> PlatformAuthenticateAsync(Uri url, Uri callbackUrl)
static async Task<WebAuthenticatorResult> PlatformAuthenticateAsync(Uri url, Uri callbackUrl)
{
var packageName = Platform.AppContext.PackageName;
@ -77,58 +69,63 @@ namespace Xamarin.Essentials
tcsResponse.TrySetCanceled();
tcsResponse = new TaskCompletionSource<WebAuthenticatorResult>();
tcsResponse.Task.ContinueWith(t =>
currentRedirectUri = callbackUrl;
var parentActivity = Platform.GetCurrentActivity(true);
var customTabsActivityManager = CustomTabsActivityManager.From(parentActivity);
try
{
// Cleanup when done
if (CustomTabsActivityManager != null)
if (await BindServiceAsync(customTabsActivityManager))
{
CustomTabsActivityManager.NavigationEvent -= CustomTabsActivityManager_NavigationEvent;
CustomTabsActivityManager.CustomTabsServiceConnected -= CustomTabsActivityManager_CustomTabsServiceConnected;
var customTabsIntent = new CustomTabsIntent.Builder(customTabsActivityManager.Session)
.SetShowTitle(true)
.Build();
try
{
CustomTabsActivityManager?.Client?.Dispose();
}
finally
{
CustomTabsActivityManager = null;
}
customTabsIntent.Intent.SetData(global::Android.Net.Uri.Parse(url.OriginalString));
WebAuthenticatorIntermediateActivity.StartActivity(parentActivity, customTabsIntent.Intent);
}
else
{
// Fall back to opening the system browser if necessary
var browserIntent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse(url.OriginalString));
Platform.CurrentActivity.StartActivity(browserIntent);
}
});
uri = url;
RedirectUri = callbackUrl;
CustomTabsActivityManager = CustomTabsActivityManager.From(Platform.GetCurrentActivity(true));
CustomTabsActivityManager.NavigationEvent += CustomTabsActivityManager_NavigationEvent;
CustomTabsActivityManager.CustomTabsServiceConnected += CustomTabsActivityManager_CustomTabsServiceConnected;
if (!CustomTabsActivityManager.BindService())
return await tcsResponse.Task;
}
finally
{
// Fall back to opening the system browser if necessary
var browserIntent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse(url.OriginalString));
Platform.CurrentActivity.StartActivity(browserIntent);
try
{
customTabsActivityManager.Client?.Dispose();
}
finally
{
}
}
}
static Task<bool> BindServiceAsync(CustomTabsActivityManager manager)
{
var tcs = new TaskCompletionSource<bool>();
manager.CustomTabsServiceConnected += OnCustomTabsServiceConnected;
if (!manager.BindService())
{
manager.CustomTabsServiceConnected -= OnCustomTabsServiceConnected;
tcs.TrySetResult(false);
}
return WebAuthenticator.ResponseTask;
return tcs.Task;
void OnCustomTabsServiceConnected(ComponentName name, CustomTabsClient client)
{
manager.CustomTabsServiceConnected -= OnCustomTabsServiceConnected;
tcs.TrySetResult(true);
}
}
static void CustomTabsActivityManager_CustomTabsServiceConnected(ComponentName name, CustomTabsClient client)
{
var builder = new CustomTabsIntent.Builder(CustomTabsActivityManager.Session)
.SetShowTitle(true);
var customTabsIntent = builder.Build();
customTabsIntent.Intent.AddFlags(ActivityFlags.SingleTop | ActivityFlags.NoHistory | ActivityFlags.NewTask);
var ctx = Platform.CurrentActivity;
CustomTabsHelper.AddKeepAliveExtra(ctx, customTabsIntent.Intent);
customTabsIntent.LaunchUrl(ctx, global::Android.Net.Uri.Parse(uri.OriginalString));
}
static void CustomTabsActivityManager_NavigationEvent(int navigationEvent, global::Android.OS.Bundle extras) =>
System.Diagnostics.Debug.WriteLine($"CustomTabs.NavigationEvent: {navigationEvent}");
}
}

Просмотреть файл

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
using Android.App;
using Android.App;
using Android.Content;
using Android.OS;
namespace Xamarin.Essentials
@ -12,8 +10,13 @@ namespace Xamarin.Essentials
{
base.OnCreate(savedInstanceState);
WebAuthenticator.OnResume(Intent);
// start the intermediate activity again with flags to close the custom tabs
var intent = new Intent(this, typeof(WebAuthenticatorIntermediateActivity));
intent.SetData(Intent.Data);
intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
StartActivity(intent);
// finish this activity
Finish();
}
}

Просмотреть файл

@ -0,0 +1,72 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
namespace Xamarin.Essentials
{
[Activity(ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
class WebAuthenticatorIntermediateActivity : Activity
{
const string launchedExtra = "launched";
const string actualIntentExtra = "actual_intent";
bool launched;
Intent actualIntent;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
var extras = savedInstanceState ?? Intent.Extras;
// read the values
launched = extras.GetBoolean(launchedExtra, false);
actualIntent = extras.GetParcelable(actualIntentExtra) as Intent;
}
protected override void OnResume()
{
base.OnResume();
if (!launched)
{
// if this is the first time, start the authentication flow
StartActivity(actualIntent);
launched = true;
}
else
{
// otherwise, resume the auth flow and finish this activity
WebAuthenticator.OnResume(Intent);
Finish();
}
}
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
Intent = intent;
}
protected override void OnSaveInstanceState(Bundle outState)
{
// save the values
outState.PutBoolean(launchedExtra, launched);
outState.PutParcelable(actualIntentExtra, actualIntent);
base.OnSaveInstanceState(outState);
}
public static void StartActivity(Activity activity, Intent intent)
{
var intermediateIntent = new Intent(activity, typeof(WebAuthenticatorIntermediateActivity));
intermediateIntent.PutExtra(actualIntentExtra, intent);
activity.StartActivity(intermediateIntent);
}
}
}

Просмотреть файл

@ -76,7 +76,7 @@ Task("docs")
.Does(() =>
{
MSBuild("./Xamarin.Essentials/Xamarin.Essentials.csproj", new MSBuildSettings()
.EnableBinaryLogger("./output/binlogs/nugets.binlog")
.EnableBinaryLogger("./output/binlogs/docs.binlog")
.SetConfiguration("Release")
.WithRestore()
.WithTarget("mdocupdatedocs"));