Skip to content

Commit

Permalink
[SDK-3348] Implement trusted web activity support (#631)
Browse files Browse the repository at this point in the history
* Trusted web activity implementation

* Add todo documentation

* Added documentation for TWA

* Fixed broken tests

* Removed unwanted imports

* Convert Note to Warning

* Add test to authenticate as TWA

* Inject TwaLauncher dependency

* Add test to check launch uri with twa

* Added tests for TWA custom tabs controller

* Update EXAMPLES.md

Co-authored-by: Jim Anderson <jim.anderson@auth0.com>

* primitive doesn't need non null annotation

* Update EXAMPLES.md

---------

Co-authored-by: Jim Anderson <jim.anderson@auth0.com>
  • Loading branch information
poovamraj and jimmyjames committed Feb 7, 2023
1 parent 54033f1 commit 2764fc9
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 35 deletions.
27 changes: 27 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [Specify Connection scope](#specify-connection-scope)
- [Customize the Custom Tabs UI](#customize-the-custom-tabs-ui)
- [Changing the Return To URL scheme](#changing-the-return-to-url-scheme)
- [Trusted Web Activity](#trusted-web-activity-experimental)
- [Authentication API](#authentication-api)
- [Login with database connection](#login-with-database-connection)
- [Login using MFA with One Time Password code](#login-using-mfa-with-one-time-password-code)
Expand Down Expand Up @@ -119,6 +120,32 @@ WebAuthProvider.logout(account)
.start(this, logoutCallback)
```

## Trusted Web Activity (Experimental)
> **Warning**
> Trusted Web Activity support in Auth0.Android is still experimental and can change in the future.
>
> Please test it thoroughly in all the targeted browsers and OS variants and let us know your feedback.
Trusted Web Activity is a feature provided by some browsers to provide a native look and feel to the custom tabs.

To use this feature, there are some additional steps you must take:

- We need the SHA256 fingerprints of the app’s signing certificate. To get this, you can run the following command on your APK
```shell
keytool -printcert -jarfile sample-debug.apk
```
- The fingerprint has to be updated in the [Auth0 Dashboard](https://manage.auth0.com/dashboard/eu/poovamraj/applications) under
Applications > *Specific Application* > Settings > Advanced Settings > Device Settings > Key Hashes
- App's package name has to be entered in the field above

Once the above prerequisites are met, you can call your login method as below to open your web authentication in Trusted Web Activity.

```kotlin
WebAuthProvider.login(account)
.withTrustedWebActivity()
.await(this)
```

## Authentication API

The client provides methods to authenticate the user against the Auth0 server.
Expand Down
1 change: 1 addition & 0 deletions auth0/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.2.2'

testImplementation 'junit:junit:4.13.2'
testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.auth0.android.annotation;

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PACKAGE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.CLASS;

import androidx.annotation.RequiresOptIn;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* The APIs marked with this annotation are considered experimental
* The API surface or design could change in future
*/
@Retention(CLASS)
@Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
public @interface ExperimentalAuth0Api {}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import com.auth0.android.provider.WebAuthProvider.resume
import com.google.androidbrowserhelper.trusted.TwaLauncher

public open class AuthenticationActivity : Activity() {
private var intentLaunched = false
Expand Down Expand Up @@ -66,17 +67,18 @@ public open class AuthenticationActivity : Activity() {
val extras = intent.extras
val authorizeUri = extras!!.getParcelable<Uri>(EXTRA_AUTHORIZE_URI)
val customTabsOptions: CustomTabsOptions = extras.getParcelable(EXTRA_CT_OPTIONS)!!
val launchAsTwa: Boolean = extras.getBoolean(EXTRA_LAUNCH_AS_TWA, false)
customTabsController = createCustomTabsController(this, customTabsOptions)
customTabsController!!.bindService()
customTabsController!!.launchUri(authorizeUri!!)
customTabsController!!.launchUri(authorizeUri!!, launchAsTwa)
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal open fun createCustomTabsController(
context: Context,
options: CustomTabsOptions
): CustomTabsController {
return CustomTabsController(context, options)
return CustomTabsController(context, options, TwaLauncher(context))
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
Expand All @@ -86,17 +88,20 @@ public open class AuthenticationActivity : Activity() {

internal companion object {
const val EXTRA_AUTHORIZE_URI = "com.auth0.android.EXTRA_AUTHORIZE_URI"
const val EXTRA_LAUNCH_AS_TWA = "com.auth0.android.EXTRA_LAUNCH_AS_TWA"
const val EXTRA_CT_OPTIONS = "com.auth0.android.EXTRA_CT_OPTIONS"
private const val EXTRA_INTENT_LAUNCHED = "com.auth0.android.EXTRA_INTENT_LAUNCHED"

@JvmStatic
internal fun authenticateUsingBrowser(
context: Context,
authorizeUri: Uri,
launchAsTwa: Boolean,
options: CustomTabsOptions
) {
val intent = Intent(context, AuthenticationActivity::class.java)
intent.putExtra(EXTRA_AUTHORIZE_URI, authorizeUri)
intent.putExtra(EXTRA_LAUNCH_AS_TWA, launchAsTwa)
intent.putExtra(EXTRA_CT_OPTIONS, options)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(intent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;

import com.google.androidbrowserhelper.trusted.TwaLauncher;

import java.lang.ref.WeakReference;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
Expand All @@ -28,18 +30,22 @@ class CustomTabsController extends CustomTabsServiceConnection {
private final AtomicReference<CustomTabsSession> session;
private final CountDownLatch sessionLatch;
private final String preferredPackage;
private final TwaLauncher twaLauncher;

@NonNull
private final CustomTabsOptions customTabsOptions;
private boolean didTryToBind;
@VisibleForTesting
boolean launchedAsTwa;

@VisibleForTesting
CustomTabsController(@NonNull Context context, @NonNull CustomTabsOptions options) {
CustomTabsController(@NonNull Context context, @NonNull CustomTabsOptions options, @NonNull TwaLauncher twaLauncher) {
this.context = new WeakReference<>(context);
this.session = new AtomicReference<>();
this.sessionLatch = new CountDownLatch(1);
this.customTabsOptions = options;
this.preferredPackage = options.getPreferredPackage(context.getPackageManager());
this.twaLauncher = twaLauncher;
}

@VisibleForTesting
Expand Down Expand Up @@ -86,6 +92,9 @@ public void unbindService() {
context.unbindService(this);
didTryToBind = false;
}
if(launchedAsTwa) {
twaLauncher.destroy();
}
}

/**
Expand All @@ -98,29 +107,44 @@ public void unbindService() {
*
* @param uri the uri to open in a Custom Tab or Browser.
*/
public void launchUri(@NonNull final Uri uri) {
public void launchUri(@NonNull final Uri uri, final boolean launchAsTwa) {
final Context context = this.context.get();
if (context == null) {
Log.v(TAG, "Custom Tab Context was no longer valid.");
return;
}

new Thread(() -> {
boolean available = false;
try {
available = sessionLatch.await(preferredPackage == null ? 0 : MAX_WAIT_TIME_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {
}
Log.d(TAG, "Launching URI. Custom Tabs available: " + available);

final Intent intent = customTabsOptions.toIntent(context, session.get());
intent.setData(uri);
try {
context.startActivity(intent);
if (launchAsTwa) {
this.launchedAsTwa = true;
twaLauncher.launch(
customTabsOptions.toTwaIntentBuilder(context, uri),
null,
null,
null,
TwaLauncher.CCT_FALLBACK_STRATEGY
);
} else {
launchAsDefault(context, uri);
}
} catch (ActivityNotFoundException ex) {
Log.e(TAG, "Could not find any Browser application installed in this device to handle the intent.");
}
}).start();
}

private void launchAsDefault(Context context, Uri uri) {
bindService();
boolean available = false;
try {
available = sessionLatch.await(preferredPackage == null ? 0 : MAX_WAIT_TIME_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {
}
Log.d(TAG, "Launching URI. Custom Tabs available: " + available);
final Intent intent = customTabsOptions.toIntent(context, session.get());
intent.setData(uri);
context.startActivity(intent);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.ColorRes;
Expand All @@ -12,6 +13,8 @@
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.trusted.TrustedWebActivityIntent;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import androidx.core.content.ContextCompat;

import com.auth0.android.authentication.AuthenticationException;
Expand Down Expand Up @@ -66,6 +69,18 @@ Intent toIntent(@NonNull Context context, @Nullable CustomTabsSession session) {
return builder.build().intent;
}

@SuppressLint("ResourceType")
TrustedWebActivityIntentBuilder toTwaIntentBuilder(@NonNull Context context, @NonNull Uri uri) {
TrustedWebActivityIntentBuilder builder = new TrustedWebActivityIntentBuilder(uri);
if (toolbarColor > 0) {
//Resource exists
final CustomTabColorSchemeParams.Builder colorBuilder = new CustomTabColorSchemeParams.Builder()
.setToolbarColor(ContextCompat.getColor(context, toolbarColor));
builder.setDefaultColorSchemeParams(colorBuilder.build());
}
return builder;
}

protected CustomTabsOptions(@NonNull Parcel in) {
showTitle = in.readByte() != 0;
toolbarColor = in.readInt();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ internal class LogoutManager(
private val callback: Callback<Void?, AuthenticationException>,
returnToUrl: String,
ctOptions: CustomTabsOptions,
federated: Boolean = false
federated: Boolean = false,
private val launchAsTwa: Boolean = false,
) : ResumableManager() {
private val parameters: MutableMap<String, String>
private val ctOptions: CustomTabsOptions
fun startLogout(context: Context) {
addClientParameters(parameters)
val uri = buildLogoutUri()
AuthenticationActivity.authenticateUsingBrowser(context, uri, ctOptions)
AuthenticationActivity.authenticateUsingBrowser(context, uri, launchAsTwa, ctOptions)
}

public override fun resume(result: AuthorizeResult): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.net.Uri
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.VisibleForTesting
import com.auth0.android.Auth0
import com.auth0.android.Auth0Exception
Expand All @@ -21,7 +22,8 @@ internal class OAuthManager(
private val account: Auth0,
private val callback: Callback<Credentials, AuthenticationException>,
parameters: Map<String, String>,
ctOptions: CustomTabsOptions
ctOptions: CustomTabsOptions,
private val launchAsTwa: Boolean = false,
) : ResumableManager() {
private val parameters: MutableMap<String, String>
private val headers: MutableMap<String, String>
Expand Down Expand Up @@ -62,7 +64,7 @@ internal class OAuthManager(
addValidationParameters(parameters)
val uri = buildAuthorizeUri()
this.requestCode = requestCode
AuthenticationActivity.authenticateUsingBrowser(context, uri, ctOptions)
AuthenticationActivity.authenticateUsingBrowser(context, uri, launchAsTwa, ctOptions)
}

fun setHeaders(headers: Map<String, String>) {
Expand Down
38 changes: 36 additions & 2 deletions auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.net.Uri
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.auth0.android.Auth0
import com.auth0.android.annotation.ExperimentalAuth0Api
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.storage.CredentialsManagerException
import com.auth0.android.callback.Callback
Expand Down Expand Up @@ -91,6 +92,7 @@ public object WebAuthProvider {
private var returnToUrl: String? = null
private var ctOptions: CustomTabsOptions = CustomTabsOptions.newBuilder().build()
private var federated: Boolean = false
private var launchAsTwa: Boolean = false

/**
* When using a Custom Tabs compatible Browser, apply these customization options.
Expand Down Expand Up @@ -148,6 +150,18 @@ public object WebAuthProvider {
return this
}

/**
* Launches the Logout experience with a native feel (without address bar). For this to work,
* you have to setup the app as trusted following the steps mentioned [here](https://github.com/auth0/Auth0.Android/blob/main/EXAMPLES.md#trusted-web-activity-experimental).
*
* This is still an experimental feature, test it thoroughly in the targeted devices and OS variants and let us know your feedback
*/
@ExperimentalAuth0Api
public fun withTrustedWebActivity(): LogoutBuilder {
launchAsTwa = true
return this
}

/**
* Request the user session to be cleared. When successful, the callback will get invoked.
* An error is raised if there are no browser applications installed in the device or if
Expand Down Expand Up @@ -175,7 +189,14 @@ public object WebAuthProvider {
account.getDomainUrl()
)
}
val logoutManager = LogoutManager(account, callback, returnToUrl!!, ctOptions, federated)
val logoutManager = LogoutManager(
account,
callback,
returnToUrl!!,
ctOptions,
federated,
launchAsTwa
)
managerInstance = logoutManager
logoutManager.startLogout(context)
}
Expand Down Expand Up @@ -219,6 +240,7 @@ public object WebAuthProvider {
private var invitationUrl: String? = null
private var ctOptions: CustomTabsOptions = CustomTabsOptions.newBuilder().build()
private var leeway: Int? = null
private var launchAsTwa: Boolean = false

/**
* Use a custom state in the requests
Expand Down Expand Up @@ -422,6 +444,18 @@ public object WebAuthProvider {
return this
}

/**
* Launches the Login experience with a native feel (without address bar). For this to work,
* you have to setup the app as trusted following the steps mentioned [here](https://github.com/auth0/Auth0.Android/blob/main/EXAMPLES.md#trusted-web-activity-experimental).
*
* This is still an experimental feature, test it thoroughly in the targeted devices and OS variants and let us know your feedback
*/
@ExperimentalAuth0Api
public fun withTrustedWebActivity(): Builder {
launchAsTwa = true
return this
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun withPKCE(pkce: PKCE): Builder {
this.pkce = pkce
Expand Down Expand Up @@ -468,7 +502,7 @@ public object WebAuthProvider {
values[OAuthManager.KEY_ORGANIZATION] = organizationId
values[OAuthManager.KEY_INVITATION] = invitationId
}
val manager = OAuthManager(account, callback, values, ctOptions)
val manager = OAuthManager(account, callback, values, ctOptions, launchAsTwa)
manager.setHeaders(headers)
manager.setPKCE(pkce)
manager.setIdTokenVerificationLeeway(leeway)
Expand Down

0 comments on commit 2764fc9

Please sign in to comment.