OidcClient2 - 等待 LoginAsync 时关闭 IBrowser

2024-02-06

目前我正在开发一个 Xamarin 应用程序,它使用 IdentityModel.OidcClient 对我的服务器进行身份验证,并且正在使用文档中提供的自动模式来完成(https://github.com/IdentityModel/IdentityModel.OidcClient2 https://github.com/IdentityModel/IdentityModel.OidcClient2)。一切都工作得很好var result = await client.LoginAsync();返回带有 AccessToken 的 LoginResult 等。

我想弄清楚的是应该如何处理后退按钮、最近的应用程序按钮(均在 Android 上)和 ChromeCustomTabsBrowser 上的关闭按钮,因为这三个操作会关闭附加到 oidcClient 的 Ibrowser 而不返回响应,这会让我陷入困境等待响应阻止我处理其余的代码验证。

   private async Task SignInAsync() {
        IsBusy = true;

        await Task.Delay(500);

        try {
            LoginResult result = await IdentityService.LoginAsync(new LoginRequest());

            if (result == null) {
                OnError(noInternetErrorMessage);
                IsBusy = false;
                return;
            }

            if (result.IsError) {
                OnError(result.Error);
            } else {
                string userName = result.User.Claims.Where(claim => claim.Type == userNameClaimType).Select(claim => claim.Value).SingleOrDefault();
                _UserToken = IdentityService.CreateOrUpdateUserToken(userName, result);

                if (_UserToken != null) {
                    await NavigationService.NavigateToAsync<LockScreenViewModel>();
                } else {
                    OnError(errorMessage);
                }
            }
        } catch (Exception e) {
            OnError(e.ToString());
        }

        IsBusy = false;
    }

在上一个代码块中我无法到达if (result == null)如果单击这些按钮,这又会阻止我删除登录视图中的 ActivityIndi​​cator 并向用户提供登录按钮,以便他可以再次尝试登录。


发生这种情况是因为您的IdentityService.LoginAsync()任务实际上仍在后台等待自定义选项卡活动回调发生,无论自定义选项卡浏览器不再可见。由于用户在完成登录往返之前关闭,因此在用户未来尝试完成往返之前,不会触发回调。每次登录尝试都会创建一个新的等待任务,因此每次用户过早关闭自定义选项卡窗口时,等待任务的集合都会增加。

当用户实际完成登录往返时,很明显所有任务仍在等待,因为当等待已久的回调最终发生时,它们全部立即解冻。这提出了另一个需要处理的问题,因为除了最后一个任务之外的所有任务都会导致'invalid state'oidc 错误结果。

我通过在开始新的登录尝试之前取消上一个任务来解决此问题。我添加了一个TryCancel方法ChromeCustomTabsBrowser在自定义界面上IBrowserExtra。在里面ChromeCustomTabsBrowser.InvokeAsync实施,保留参考TaskCompletionSource被退回。 下次用户单击登录按钮时,TryCancel之前首先被调用ChromeCustomTabsBrowser.LoginAsync使用保留的参考来解锁仍在等待的先前登录尝试。

为了使这项工作顺利进行,IsBusy=True应推迟到自定义选项卡回调之后(自定义选项卡浏览器无论如何都会位于顶部),以在单击自定义选项卡关闭按钮时保持 GUI 交互。否则用户将永远无法重新尝试登录。

Update:根据要求添加了示例代码。

public interface IBrowserExtra
{
    void TryCancel();
}

public class ChromeCustomTabsBrowser : IBrowser, IBrowserExtra, IBrowserFallback
{
    private readonly Activity _context;
    private readonly CustomTabsActivityManager _manager;
    private TaskCompletionSource<BrowserResult> _task; 
    private Action<string> _callback;

    public ChromeCustomTabsBrowser()
    {
        _context = CrossCurrentActivity.Current.Activity;
        _manager = new CustomTabsActivityManager(_context);
    }

    public Task<BrowserResult> InvokeAsync(BrowserOptions options)
    {
        var builder = new CustomTabsIntent.Builder(_manager.Session)
            .SetToolbarColor(Color.Argb(255, 0, 0, 0))
            .SetShowTitle(false)
            .EnableUrlBarHiding()
            .SetStartAnimations(_context, Android.Resource.Animation.SlideInLeft, Android.Resource.Animation.SlideOutRight)
            .SetExitAnimations(_context, Android.Resource.Animation.SlideInLeft, Android.Resource.Animation.SlideOutRight);
        var customTabsIntent = builder.Build();

        // ensures the intent is not kept in the history stack, which makes
        // sure navigating away from it will close it
        customTabsIntent.Intent.AddFlags(ActivityFlags.NoHistory);

        _callback = null;
        _callback = url =>
        {
            UnsubscribeFromCallback();

            _task.TrySetResult(new BrowserResult()
            {
                Response = url
            });
        };

        SubscribeToCallback();

        // Keep track of this task to be able to refer it from TryCancel later 
        _task = new TaskCompletionSource<BrowserResult>();

        customTabsIntent.LaunchUrl(_context, Android.Net.Uri.Parse(options.StartUrl));

        return _task.Task;
    }

    private void SubscribeToCallback()
    {
        OidcCallbackActivity.Callbacks += _callback;
    }

    private void UnsubscribeFromCallback()
    {
        OidcCallbackActivity.Callbacks -= _callback;
        _callback = null;
    }

    void IBrowserExtra.TryCancel()
    {
        if (_callback != null)
        {
            UnsubscribeFromCallback();
        }

        if (_task != null)
        {
            _task.TrySetCanceled();
            _task = null;
        }
    }
}

public class LoginService
{
    private static OidcClient s_loginClient;
    private Task<LoginResult> _loginChallengeTask;

    private readonly IBrowser _browser;
    private readonly IAppInfo _appInfo;

    public LoginService(
        IBrowser secureBrowser,
        IBrowserFallback fallbackBrowser,
        IAppInfo appInfo)
    {
        _appInfo = appInfo;

        _browser = ChooseBrowser(appInfo, secureBrowser, fallbackBrowser);
    }

    private IBrowser ChooseBrowser(IAppInfo appInfo, IBrowser secureBrowser, IBrowserFallback fallbackBrowser)
    {
        return appInfo.PlatformSupportsSecureBrowserSession ? secureBrowser : fallbackBrowser as IBrowser;
    }

    public async Task<bool> StartLoginChallenge()
    {
        // Cancel any pending browser invocation task
        EnsureNoLoginChallengeActive();

        s_loginClient = OpenIdConnect.CreateOidcClient(_browser, _appInfo);

        try
        {
            _loginChallengeTask = s_loginClient.LoginAsync(new LoginRequest()
            {
                FrontChannelExtraParameters = OpenIdConnectConfiguration.LoginExtraParams
            });

            // This triggers the custom tabs browser login session
            var oidcResult = await _loginChallengeTask;

            if (_loginChallengeTask.IsCanceled)
            {
                // task can be cancelled if a second login attempt was completed while the first 
                // was cancelled prematurely even before the browser view appeared.
                return false;
            }
            else
            {
                // at this point we returned from the browser login session
                if (oidcResult?.IsError ?? throw new LoginException("oidcResult is null."))
                {
                    if (oidcResult.Error == "UserCancel")
                    {
                        // Graceful exit: user canceled using the close button on the browser view.
                        return false;
                    }
                    else
                    {
                        throw new LoginException(oidcResult.Error);
                    }
                }
            }

            // we get here if browser session just popped and navigation is back at customer page
            PerformPostLoginOperations(oidcResult);

            return true;
        }
        catch (TaskCanceledException)
        {
            // swallow cancel exception.
            // this can occur when user canceled browser session and restarted. 
            // Previous session is forcefully canceled at start of ExecuteLoginChallenge cauing this exception.
            LogHelper.Debug($"'Login attempt was via browser roundtrip canceled.");
            return false;
        }
    }

    private void EnsureNoLoginChallengeActive()
    {
        if (IsLoginSessionStarted)
        {
            (_browser as IBrowserExtra)?.TryCancel();
        }
    }

    private static bool IsLoginSessionStarted => s_loginClient != null;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

OidcClient2 - 等待 LoginAsync 时关闭 IBrowser 的相关文章

随机推荐