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¶m2=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¶m2=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.
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.
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); }
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With