Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Navigation is blocked error in Chrome 61+ on Android

I'm building a login page that, upon submitting and validation of the user credentials, opens up a native mobile application. Up till last week, I had this working cross mobile OS by using a custom scheme URI, something like:

function onLoginCallback() {     const redirectUri = 'customscheme:/action?param1=val1&param2=val2';     window.location.replace(redirectUri); } 

The login page is displayed in an IABT, short for In App Browser Tab.

However, since the release of version 61 of Chrome, this is approach is broken on Android. Chrome blocks the redirect because there's no apparent user action related to the redirect (see here for more information on the matter).

As a consequence, when executing the code above, I'll end up with a warning in the console:

Navigation is blocked: customscheme:/action?param1=val1&param2=val2

I've also tried updating the custom scheme url to an intent url but to no avail. Googling about this issue doesn't readily provide a clear solution, so I'm hoping anyone on can help me out.


Edit: Tried to reproduce the issue with the following scenario (as close as possible to the real life scenario):
  • IABT displays a page with a single button
  • Clicking the button fires an jsonp call to a mock endpoint
  • The JSONP callback is executed and fires off a custom event
  • An event handler for the custom event is triggered and redirects the browser to another mock endpoint
  • That mock endpoint responds with a 302 to the custom deeplink scheme.

Alas, this seems to be working. I would have expected that the inclusion of the jsonp call would cause Chrome to block the final redirect as it would not be able to identify it as a user initiated action.


Edit 2: Managed to get a reproducible scenario. We've set up a dummy endpoint, that upon request simply returns a 302 with the custom scheme in the Location header. This is blocked on all tries, except for the first one. That fact still boggles the mind. We're using the AppAuth for Android application to test the setup.

I'm opening a custom tab to the endpoint as shown below. The code is taken from this answer.

void launchTab(Context context, Uri uri){     final CustomTabsServiceConnection connection = new CustomTabsServiceConnection() {         @Override         public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) {             final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();             final CustomTabsIntent intent = builder.build();             client.warmup(0L); // This prevents backgrounding after redirection             intent.launchUrl(context, uri);         }         @Override         public void onServiceDisconnected(ComponentName name) {          }     };     CustomTabsClient.bindCustomTabsService(context, "com.android.chrome", connection); } 
like image 742
thomaux Avatar asked Oct 13 '17 10:10

thomaux


2 Answers

We ended up implementing our login and registration forms with a classic post-redirect-get pattern.

The server responds with a 302 to the custom URI scheme. Because in this setup there's no asynchronous execution between the user submitting the form and the browser receiving a redirect, Chrome correctly identifies the chain of actions as trusted and thus will not block the navigation.

I realise this might not be the preferred solution for everyone. A possible alternative to support asynchronous execution flows is the use of universal links as these use regular http(s) schemes, to which redirects were (at the time of posting my question) not considered harmful by Chrome.

like image 119
thomaux Avatar answered Oct 06 '22 09:10

thomaux


For those who use App Auth client and Identity Server:

Startup.cs

services.AddTransient<IAuthorizeResponseGenerator, AuthorizeRG>(); 

AuthorizeRG.cs

public class AuthorizeRG: AuthorizeResponseGenerator  {     public override async Task<AuthorizeResponse> CreateResponseAsync(ValidatedAuthorizeRequest request)     {         var response = await base.CreateResponseAsync(request);         if (response.RedirectUri != null && request.IsNativeClient())              //this fix chrome navigation blocked on native clients https://bugs.chromium.org/p/chromium/issues/detail?id=738724             response.Request.RedirectUri = $"/native/redirect/{HttpUtility.UrlEncode(response.RedirectUri)}";          return response;     } } 

NativeController.cs

[Route("[controller]")] public class NativeController : Controller {     [HttpGet("Redirect/{redirectUri}")]     public IActionResult Redirect([FromRoute] string redirectUri)     {         redirectUri = HttpUtility.UrlDecode(redirectUri);         redirectUri += HttpContext.Request.QueryString.ToUriComponent();         return this.LoadingPage("Redirect", redirectUri);     } } 

Extensions.cs

    /// <summary>     /// Checks if the redirect URI is for a native client.     /// </summary>     /// <returns></returns>     public static bool IsNativeClient(this AuthorizationRequest context)     {         return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)                && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);     }      public static bool IsNativeClient(this ValidatedAuthorizeRequest context)     {         return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)                && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);     }      public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri)     {         controller.HttpContext.Response.StatusCode = 200;         controller.HttpContext.Response.Headers["Location"] = "";                  return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri });     } 

This works for me, but please comment if it broke smth in your authorization flow

like image 30
alloha Avatar answered Oct 06 '22 09:10

alloha