openid / AppAuth-Android

Android client SDK for communicating with OAuth 2.0 and OpenID Connect providers.
https://openid.github.io/AppAuth-Android
Apache License 2.0
2.86k stars 887 forks source link

Add support for Trusted Web Activity for self-IDP authorization #292

Open iainmcgin opened 7 years ago

iainmcgin commented 7 years ago

Trusted Web Activity was announced at the 2017 Chrome Developer Summit - this provides a WebView-like (i.e. decoration-less) experience for web content that is affiliated (via digital asset links) with the app. The trusted web activity has access to standalone browser state, like a custom tab.

This will be of particular interest to users of AppAuth who are authenticating with their own OAuth2 / OpenID Connect implementation. We have seen numerous requests for a WebView-like experience in AppAuth for self-IDP authorization, and we can now provide this by using Trusted Web Activity when applicable.

iainmcgin commented 7 years ago

Looks like support library 27.0.0 has been released, and the new class (TrustedWebUtils) is available. Will experiment with this when I have time.

jcayzac commented 5 years ago

Chrome 72 for Android has shipped, so trusted web activities are no longer experimental.

jcayzac commented 5 years ago

The documentation above looks outdated. You can now simply launch a custom tab intent as a trusted web activity using TrustedWebUtils#launchAsTrustedWebActivity(Context, CustomTabsIntent, Uri).

jcayzac commented 5 years ago

@iainmcgin actually the utils class takes care of launching the intent, which seems to imply a bit of refactoring in this library's source code.

On the other hand, all that launchAsTrustedWebActivity() method does it to simply add extras to the custom tabs intent, all of which are public statics:

public static void launchAsTrustedWebActivity(Context context, CustomTabsIntent intent, Uri uri, @Nullable List<String> additionalTrustedOrigins) {
    if (!intent.intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)) {
        throw new IllegalArgumentException("The CustomTabsIntent should be associated with a " + "CustomTabsSession");
    }
    intent.intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true);
    if (additionalTrustedOrigins != null) {
        intent.intent.putExtra(TrustedWebUtils.EXTRA_ADDITIONAL_TRUSTED_ORIGINS, new ArrayList<>(additionalTrustedOrigins));
    }
    intent.launchUrl(context, uri);
}

So doing the same here would mean a very minimal change.

jcayzac commented 5 years ago

Actually… is there anything to be done in AppAuth? It seems to me the app can simply use the normal custom tabs support and add the extras by itself…? It's certainly a minor change in the demo app to enable TWA.

emilvandenberg commented 4 years ago

It is indeed only some small steps to get it working, at least, partially:

1) replace in LoginActivity, in the warmupBrowser method the line

mAuthIntent.set(intentBuilder.build());

by these lines:

CustomTabsIntent intent = intentBuilder.build(); intent.intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true); mAuthIntent.set(intent);

2) link your app to the site using Digital Asset Links: https://developers.google.com/digital-asset-links/v1/getting-started or https://developers.google.com/web/android/trusted-web-activity

However, there is one problem: not when you login first time; then it works as expected. But if you log out from the app (but not from the site), and then try to relogin, then it opens a blank screen in stead of the expected site. This can be triggered by clicking the sign out button in the demo app, or by uninstalling the demo app and reinstalling.

There is a workaround: when detecting that the app is not logged in (the isAuthorized() check in onCreate), then first do a logout in the browser, redirect back to the app, and then proceed the normal flow.

The problem can easily be reproduced using the demo app with this configuration (auth_config.json):

{ "client_id": "interactive.public", "redirect_uri": "io.identityserver.demo:/oauthredirect", "authorization_scope": "openid email profile", "discovery_uri": "", "authorization_endpoint_uri": "https://demo.identityserver.io/connect/authorize", "token_endpoint_uri": "https://demo.identityserver.io/connect/token", "registration_endpoint_uri": "", "user_info_endpoint_uri": "https://demo.identityserver.io/connect/userinfo", "https_required": true }

And in app/build.gradle:

manifestPlaceholders = [ 'appAuthRedirectScheme': 'io.identityserver.demo' ]

Of course TWA will not really work this way, because the app is not linked to that server, but the problem will still appear.

Has anybody an idea why this happens and how it can be fixed without using the workaround?

alllohaaa commented 4 years ago

If you use your own IdentityServer as a backend you may try my solution, but I'm not sure if it suits every need

Nimrodda commented 3 years ago

I narrowed it down to this:

val customTabsIntent = CustomTabsIntent.Builder().build().apply {
  intent.putExtra(
    TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY,
       true
    )
}

startActivityForResult(authService.getAuthorizationRequestIntent(authRequest, customTabsIntent), 1)

It seems to work only on some devices, while on others, Google Chrome crashes. For example, it works well on Galaxy S10 Lite (Android 10), but not on Pixel 2 Android 11. I see the following exception in Logcat:

Process: com.android.chrome, PID: 13930
    java.lang.IllegalArgumentException: Unknown package: null
        at android.os.Parcel.readException(Parcel.java:1966)
        at android.os.Parcel.readException(Parcel.java:1899)
        at android.content.pm.IPackageManager$Stub$Proxy.getInstallerPackageName(IPackageManager.java:4205)
        at android.app.ApplicationPackageManager.getInstallerPackageName(ApplicationPackageManager.java:1851)
        at aR0.h(chromium-Monochrome.aab-stable-424019823:5)
        at aR0.j(chromium-Monochrome.aab-stable-424019823:5)
        at YQ0.onResult(chromium-Monochrome.aab-stable-424019823:3)
        at Ix0.run(chromium-Monochrome.aab-stable-424019823:1)
        at android.os.Handler.handleCallback(Handler.java:789)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at android.os.Looper.loop(Looper.java:251)
        at android.app.ActivityThread.main(ActivityThread.java:6572)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)

@jcayzac @emilvandenberg have you bumped the above exception by any chance?

ghilainm commented 2 years ago

Just for information, we have actually an issue with the library.

It seems like the way you launch the TWA is not working.

Indeed, we extracted the following snippet from the library and we cannot successfully open the TWA.

        val builderCustomTabs = CustomTabsIntent.Builder()
        val intentCustomTabs = builderCustomTabs.build()
        intentCustomTabs.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
        intentCustomTabs.intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
        intentCustomTabs.launchUrl(activity, Uri.parse(startAuthRequestUrl))

However, using TwaLauncher it works properly.

Edit: digging a bit in the documentation it seems that the recommended way to launch a TWA is the following: https://github.com/GoogleChrome/android-browser-helper/blob/main/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java

Is there anyway we could configure AppAuth to replace the way TWA is launched to use TwaLauncher?

This should be something like this:

    val builder = TrustedWebActivityIntentBuilder(Uri.parse(startAuthRequestUrl))
    val launcher = TwaLauncher(activity)
    launcher.launch(builder,null, null, null)
Nimrodda commented 2 years ago

@ghilainm what I found is that you're not supposed to instantiate your own CustomTabsIntent. Instead, you need to get it from the AuthorizationService.createCustomTabsIntentBuilder:

val customTabsIntent = authService.createCustomTabsIntentBuilder()
  .build()
  .apply {
    intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
  }

Once you have it, you need to pass it to to AuthorizationService.getAuthorizationRequestIntent:

val intent = authService.getAuthorizationRequestIntent(authRequest, customTabsIntent)

authRequest is the one you build with AuthorizationRequest.Builder.

And finally, start the Activity: activity.startActivityForResult(intent, 1231).

This has worked for me flawlessly on all devices and Android versions. Also, if the device doesn't have a browser with CustomTabs support, it will automatically fallback to opening the browser itself.