diff --git a/README.md b/README.md index 9d77e61d2..0c0bb3ff0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Android API version 15 or newer ## Installation -###Gradle +### Gradle Auth0.android is available through [Gradle](https://gradle.org/). To install it, simply add the following line to your `build.gradle` file: @@ -269,60 +269,75 @@ First go to [Auth0 Dashboard](https://manage.auth0.com/#/applications) and go to https://{YOUR_AUTH0_DOMAIN}/android/{YOUR_APP_PACKAGE_NAME}/callback ``` -Open your app's `AndroidManifest.xml` file and add the following permission. +Remember to replace `{YOUR_APP_PACKAGE_NAME}` with your actual application's package name, available in your `app/build.gradle` file as the `applicationId` value. -```xml - + +Next, define a placeholder for the Auth0 Domain which is going to be used internally by the library to register an **intent-filter**. Go to your application's `build.gradle` file and add the `manifestPlaceholders` line as shown below: + +```groovy +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + defaultConfig { + applicationId "com.auth0.samples" + minSdkVersion 15 + targetSdkVersion 25 + //... + + //---> Add the next line + manifestPlaceholders = [auth0Domain: "@string/auth0_domain"] + //<--- + } + //... +} ``` -Also register the intent filters inside your activity's tag, so you can receive the call in your activity. Note that you will have to specify the callback url inside the `data` tag. +It's a good practice to define reusable resources like `@string/auth0_domain` but you can also hard code the value in the file. + +Alternatively, you can declare the `RedirectActivity` in the `AndroidManifest.xml` file with your own **intent-filter** so it overrides the library's default. If you do this then the `manifestPlaceholders` don't need to be set as long as the activity contains the `tools:node="replace"` like in the snippet below. If you choose to use a [custom scheme](#a-note-about-app-deep-linking) you must define your own intent-filter as explained below. + +In your manifest inside your application's tag add the `RedirectActivity` declaration: ```xml + - - + android:name="com.auth0.android.provider.RedirectActivity" + tools:node="replace"> + - + ``` -Make sure the Activity's **launchMode** is declared as "singleTask" or the result won't come back after the authentication. +If you request a different scheme you must replace the `android:scheme` property value. Finally, don't forget to add the internet permission. -When you launch the WebAuthProvider you'll expect a result back. To capture the response override the `onNewIntent` method and call `WebAuthProvider.resume()` with the received parameters: +```xml + +``` -```java -public class MyActivity extends Activity { - @Override - protected void onNewIntent(Intent intent) { - if (WebAuthProvider.resume(intent)) { - return; - } - super.onNewIntent(intent); - } -} +> In versions 1.8.0 and before you had to define the **intent-filter** inside your activity to capture the result in the `onNewIntent` method and call `WebAuthProvider.resume()` with the received intent. This call is no longer required for versions greater than 1.8.0 as it's now done for you by the library. -``` ##### A note about App Deep Linking: @@ -451,7 +466,7 @@ android { ref: https://github.com/square/okio/issues/58#issuecomment-72672263 -##Proguard +## Proguard The rules should be applied automatically if your application is using `minifyEnabled = true`. If you want to include them manually check the [proguard directory](proguard). By default you should at least use the following files: * `proguard-okio.pro` diff --git a/auth0/build.gradle b/auth0/build.gradle index 152195589..6ee655754 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -59,14 +59,21 @@ android { lintOptions { warning 'InvalidPackage' } + buildTypes { + debug { + //Helps tests. buildTypes values are not included in the merged manifest + manifestPlaceholders = [auth0Domain: "auth0.test.domain"] + } + } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.0.1' + compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:customtabs:25.3.1' compile 'com.squareup.okhttp:okhttp:2.7.5' compile 'com.squareup.okhttp:logging-interceptor:2.7.5' - compile 'com.google.code.gson:gson:2.6.2' + compile 'com.google.code.gson:gson:2.7' compile 'com.auth0.android:jwtdecode:1.1.0' testCompile 'junit:junit:4.12' diff --git a/auth0/src/main/AndroidManifest.xml b/auth0/src/main/AndroidManifest.xml index ccc5384fb..6f2354e4b 100644 --- a/auth0/src/main/AndroidManifest.xml +++ b/auth0/src/main/AndroidManifest.xml @@ -22,4 +22,35 @@ ~ THE SOFTWARE. --> - + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java new file mode 100644 index 000000000..8450fec02 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.java @@ -0,0 +1,120 @@ +package com.auth0.android.provider; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +public class AuthenticationActivity extends Activity { + + static final String EXTRA_USE_BROWSER = "com.auth0.android.EXTRA_USE_BROWSER"; + static final String EXTRA_USE_FULL_SCREEN = "com.auth0.android.EXTRA_USE_FULL_SCREEN"; + static final String EXTRA_CONNECTION_NAME = "com.auth0.android.EXTRA_CONNECTION_NAME"; + private static final String EXTRA_INTENT_LAUNCHED = "com.auth0.android.EXTRA_INTENT_LAUNCHED"; + + private boolean intentLaunched; + private CustomTabsController customTabsController; + + static void authenticateUsingBrowser(Context context, Uri authorizeUri) { + Intent intent = new Intent(context, AuthenticationActivity.class); + intent.setData(authorizeUri); + intent.putExtra(AuthenticationActivity.EXTRA_USE_BROWSER, true); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + context.startActivity(intent); + } + + static void authenticateUsingWebView(Activity activity, Uri authorizeUri, int requestCode, String connection, boolean useFullScreen) { + Intent intent = new Intent(activity, AuthenticationActivity.class); + intent.setData(authorizeUri); + intent.putExtra(AuthenticationActivity.EXTRA_USE_BROWSER, false); + intent.putExtra(AuthenticationActivity.EXTRA_USE_FULL_SCREEN, useFullScreen); + intent.putExtra(AuthenticationActivity.EXTRA_CONNECTION_NAME, connection); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + activity.startActivityForResult(intent, requestCode); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + deliverSuccessfulAuthenticationResult(data); + } + finish(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(EXTRA_INTENT_LAUNCHED, intentLaunched); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + intentLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (!intentLaunched) { + intentLaunched = true; + launchAuthenticationIntent(); + return; + } + + if (getIntent().getData() != null) { + deliverSuccessfulAuthenticationResult(getIntent()); + } + finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (customTabsController != null) { + customTabsController.unbindService(); + customTabsController = null; + } + } + + private void launchAuthenticationIntent() { + Bundle extras = getIntent().getExtras(); + final Uri authorizeUri = getIntent().getData(); + if (!extras.getBoolean(EXTRA_USE_BROWSER, true)) { + Intent intent = new Intent(this, WebAuthActivity.class); + intent.setData(authorizeUri); + intent.putExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, extras.getString(EXTRA_CONNECTION_NAME)); + intent.putExtra(WebAuthActivity.FULLSCREEN_EXTRA, extras.getBoolean(EXTRA_USE_FULL_SCREEN)); + //The request code value can be ignored + startActivityForResult(intent, 33); + return; + } + + customTabsController = createCustomTabsController(this); + customTabsController.bindService(); + customTabsController.launchUri(authorizeUri); + } + + @VisibleForTesting + CustomTabsController createCustomTabsController(@NonNull Context context) { + return new CustomTabsController(context); + } + + @VisibleForTesting + void deliverSuccessfulAuthenticationResult(Intent result) { + WebAuthProvider.resume(result); + } + +} diff --git a/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java b/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java new file mode 100644 index 000000000..b912c2966 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/provider/CustomTabsController.java @@ -0,0 +1,187 @@ +package com.auth0.android.provider; + +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.customtabs.CustomTabsSession; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +@SuppressWarnings("WeakerAccess") +class CustomTabsController extends CustomTabsServiceConnection { + + private static final String TAG = CustomTabsController.class.getSimpleName(); + private static final long MAX_WAIT_TIME_SECONDS = 1; + private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService"; + //Known Browsers + private static final String CHROME_STABLE = "com.android.chrome"; + private static final String CHROME_SYSTEM = "com.google.android.apps.chrome"; + private static final String CHROME_BETA = "com.android.chrome.beta"; + private static final String CHROME_DEV = "com.android.chrome.dev"; + + private final WeakReference context; + private final AtomicReference session; + private final CountDownLatch sessionLatch; + private final String preferredPackage; + + + @VisibleForTesting + CustomTabsController(@NonNull Context context, @NonNull String browserPackage) { + this.context = new WeakReference<>(context); + this.session = new AtomicReference<>(); + this.sessionLatch = new CountDownLatch(1); + this.preferredPackage = browserPackage; + } + + public CustomTabsController(@NonNull Context context) { + this(context, getBestBrowserPackage(context)); + } + + @VisibleForTesting + void clearContext() { + this.context.clear(); + } + + @Override + public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient customTabsClient) { + if (customTabsClient == null) { + return; + } + Log.d(TAG, "CustomTabs Service connected"); + customTabsClient.warmup(0L); + session.set(customTabsClient.newSession(null)); + sessionLatch.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + Log.d(TAG, "CustomTabs Service disconnected"); + session.set(null); + } + + /** + * Attempts to bind the Custom Tabs Service to the Context. + */ + public void bindService() { + Log.v(TAG, "Trying to bind the service"); + Context context = this.context.get(); + boolean success = false; + if (context != null) { + success = CustomTabsClient.bindCustomTabsService(context, preferredPackage, this); + } + Log.v(TAG, "Bind request result: " + success); + } + + /** + * Attempts to unbind the Custom Tabs Service from the Context. + */ + public void unbindService() { + Log.v(TAG, "Trying to unbind the service"); + Context context = this.context.get(); + if (context != null) { + context.unbindService(this); + } + } + + /** + * Opens a Uri in a Custom Tab or Browser. + * The Custom Tab service will be given up to {@link CustomTabsController#MAX_WAIT_TIME_SECONDS} to be connected. + * If it fails to connect the Uri will be opened on a Browser. + * + * @param uri the uri to open in a Custom Tab or Browser. + */ + public void launchUri(@NonNull final Uri uri) { + final Context context = this.context.get(); + if (context == null) { + Log.v(TAG, "Custom Tab Context was no longer valid."); + return; + } + + new Thread(new Runnable() { + @Override + public void run() { + boolean available = false; + try { + available = sessionLatch.await(MAX_WAIT_TIME_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + Log.d(TAG, "Launching URI. Custom Tabs available: " + available); + + final Intent intent = new CustomTabsIntent.Builder(session.get()) + .build() + .intent; + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.setData(uri); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException ignored) { + Intent fallbackIntent = new Intent(Intent.ACTION_VIEW, uri); + fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + context.startActivity(fallbackIntent); + } + } + }).start(); + } + + /** + * Query the OS for a Custom Tab compatible Browser application. + * It will pick the default browser first if is Custom Tab compatible, then any Chrome browser or the first Custom Tab compatible browser. + * + * @param context a valid Context + * @return the recommended Browser application package name, compatible with Custom Tabs if possible. + */ + @VisibleForTesting + static String getBestBrowserPackage(@NonNull Context context) { + PackageManager pm = context.getPackageManager(); + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + ResolveInfo webHandler = pm.resolveActivity(browserIntent, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PackageManager.MATCH_ALL : PackageManager.MATCH_DEFAULT_ONLY); + String defaultBrowser = null; + if (webHandler != null) { + defaultBrowser = webHandler.activityInfo.packageName; + } + + List resolvedActivityList = pm.queryIntentActivities(browserIntent, 0); + List customTabsBrowsers = new ArrayList<>(); + for (ResolveInfo info : resolvedActivityList) { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(info.activityInfo.packageName); + if (pm.resolveService(serviceIntent, 0) != null) { + customTabsBrowsers.add(info.activityInfo.packageName); + } + } + if (customTabsBrowsers.contains(defaultBrowser)) { + return defaultBrowser; + } else if (customTabsBrowsers.contains(CHROME_STABLE)) { + return CHROME_STABLE; + } else if (customTabsBrowsers.contains(CHROME_SYSTEM)) { + return CHROME_SYSTEM; + } else if (customTabsBrowsers.contains(CHROME_BETA)) { + return CHROME_BETA; + } else if (customTabsBrowsers.contains(CHROME_DEV)) { + return CHROME_DEV; + } else if (!customTabsBrowsers.isEmpty()) { + return customTabsBrowsers.get(0); + } else { + return defaultBrowser; + } + } + +} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java index d7e34e494..385b2b553 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.java @@ -2,7 +2,6 @@ import android.app.Activity; import android.app.Dialog; -import android.content.Intent; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -24,6 +23,7 @@ import java.util.HashMap; import java.util.Map; +@SuppressWarnings("WeakerAccess") class OAuthManager { private static final String TAG = OAuthManager.class.getSimpleName(); @@ -63,7 +63,7 @@ class OAuthManager { private PKCE pkce; private Long currentTimeInMillis; - OAuthManager(Auth0 account, AuthCallback callback, Map parameters) { + OAuthManager(@NonNull Auth0 account, @NonNull AuthCallback callback, @NonNull Map parameters) { this.account = account; this.callback = callback; this.parameters = new HashMap<>(parameters); @@ -87,21 +87,12 @@ void startAuthorization(Activity activity, String redirectUri, int requestCode) addClientParameters(parameters, redirectUri); addValidationParameters(parameters); Uri uri = buildAuthorizeUri(); - this.requestCode = requestCode; - final Intent intent; - if (this.useBrowser) { - Log.d(TAG, "About to start the authorization using the Browser"); - intent = new Intent(Intent.ACTION_VIEW, uri); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - activity.startActivity(intent); + + if (useBrowser) { + AuthenticationActivity.authenticateUsingBrowser(activity, uri); } else { - Log.d(TAG, "About to start the authorization using the WebView"); - intent = new Intent(activity, WebAuthActivity.class); - intent.setData(uri); - intent.putExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, parameters.get(KEY_CONNECTION)); - intent.putExtra(WebAuthActivity.FULLSCREEN_EXTRA, useFullScreen); - activity.startActivityForResult(intent, requestCode); + AuthenticationActivity.authenticateUsingWebView(activity, uri, requestCode, parameters.get(KEY_CONNECTION), useFullScreen); } } @@ -121,7 +112,7 @@ boolean resumeAuthorization(AuthorizeResult data) { try { assertNoError(values.get(KEY_ERROR), values.get(KEY_ERROR_DESCRIPTION)); assertValidState(parameters.get(KEY_STATE), values.get(KEY_STATE)); - if (parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_ID_TOKEN)) { + if (parameters.containsKey(KEY_RESPONSE_TYPE) && parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_ID_TOKEN)) { assertValidNonce(parameters.get(KEY_NONCE), values.get(KEY_ID_TOKEN)); } @@ -234,7 +225,7 @@ private void addValidationParameters(Map parameters) { String state = getRandomString(parameters.get(KEY_STATE)); parameters.put(KEY_STATE, state); - if (parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_ID_TOKEN)) { + if (parameters.containsKey(KEY_RESPONSE_TYPE) && parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_ID_TOKEN)) { String nonce = getRandomString(parameters.get(KEY_NONCE)); parameters.put(KEY_NONCE, nonce); } @@ -255,7 +246,7 @@ private void createPKCE(String redirectUri) { } private boolean shouldUsePKCE() { - return parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_CODE) && PKCE.isAvailable(); + return parameters.containsKey(KEY_RESPONSE_TYPE) && parameters.get(KEY_RESPONSE_TYPE).contains(RESPONSE_TYPE_CODE) && PKCE.isAvailable(); } @VisibleForTesting diff --git a/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java b/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java new file mode 100644 index 000000000..75eea1f09 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/provider/RedirectActivity.java @@ -0,0 +1,21 @@ +package com.auth0.android.provider; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +public class RedirectActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceBundle) { + super.onCreate(savedInstanceBundle); + Intent intent = new Intent(this, AuthenticationActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + if (getIntent() != null) { + intent.setData(getIntent().getData()); + } + startActivity(intent); + finish(); + } + +} diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java index d24f97561..813a4ceae 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java @@ -255,9 +255,7 @@ Builder withPKCE(PKCE pkce) { } /** - * Begins the authentication flow. - * Make sure to override your activity's onNewIntent() and onActivityResult() methods, - * and call this provider's resume() method with the received parameters. + * Request user Authentication. The result will be received in the callback. * * @param activity context to run the authentication * @param callback to receive the parsed results @@ -266,8 +264,8 @@ Builder withPKCE(PKCE pkce) { */ @Deprecated public void start(@NonNull Activity activity, @NonNull AuthCallback callback, int requestCode) { + managerInstance = null; if (account.getAuthorizeUrl() == null) { - managerInstance = null; final AuthenticationException ex = new AuthenticationException("a0.invalid_authorize_url", "Auth0 authorize URL not properly set. This can be related to an invalid domain."); callback.onFailure(ex); return; @@ -285,8 +283,7 @@ public void start(@NonNull Activity activity, @NonNull AuthCallback callback, in } /** - * Begins the authentication flow. - * Make sure to override your activity's onNewIntent() method and call this provider's resume() method with the received parameters. + * Request user Authentication. The result will be received in the callback. * * @param activity context to run the authentication * @param callback to receive the parsed results @@ -309,7 +306,6 @@ public static Builder init(@NonNull Auth0 account) { return new Builder(account); } - /** * Initialize the WebAuthProvider instance with an Android Context. Additional settings can be configured * in the Builder, like setting the connection name or authentication parameters. @@ -318,12 +314,14 @@ public static Builder init(@NonNull Auth0 account) { * @return a new Builder instance to customize. */ public static Builder init(@NonNull Context context) { - return new Builder(new Auth0(context)); + return init(new Auth0(context)); } /** * Finishes the authentication flow by passing the data received in the activity's onActivityResult() callback. * The final authentication result will be delivered to the callback specified when calling start(). + *

+ * This is no longer required to be called, the authentication is handled internally as long as you've correctly setup the intent-filter. * * @param requestCode the request code received on the onActivityResult() call * @param resultCode the result code received on the onActivityResult() call @@ -348,6 +346,8 @@ public static boolean resume(int requestCode, int resultCode, @Nullable Intent i /** * Finishes the authentication flow by passing the data received in the activity's onNewIntent() callback. * The final authentication result will be delivered to the callback specified when calling start(). + *

+ * This is no longer required to be called, the authentication is handled internally as long as you've correctly setup the intent-filter. * * @param intent the data received on the onNewIntent() call * @return true if a result was expected and has a valid format, or false if not. diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java new file mode 100644 index 000000000..f249cb913 --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityMock.java @@ -0,0 +1,34 @@ +package com.auth0.android.provider; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; + +/** + * Created by lbalmaceda on 6/12/17. + */ + +public class AuthenticationActivityMock extends AuthenticationActivity { + + private CustomTabsController customTabsController; + private Intent deliveredIntent; + + @Override + protected CustomTabsController createCustomTabsController(@NonNull Context context) { + return customTabsController; + } + + @Override + protected void deliverSuccessfulAuthenticationResult(Intent result) { + this.deliveredIntent = result; + super.deliverSuccessfulAuthenticationResult(result); + } + + public void setCustomTabsController(CustomTabsController customTabsController) { + this.customTabsController = customTabsController; + } + + public Intent getDeliveredIntent() { + return deliveredIntent; + } +} diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java new file mode 100644 index 000000000..d76a99971 --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.java @@ -0,0 +1,334 @@ +package com.auth0.android.provider; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivity; +import org.robolectric.util.ActivityController; + +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasData; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasFlag; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.robolectric.Shadows.shadowOf; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = com.auth0.android.auth0.BuildConfig.class, sdk = 18, manifest = Config.NONE) +public class AuthenticationActivityTest { + + @Mock + private Uri uri; + @Mock + private Uri resultUri; + @Mock + private CustomTabsController customTabsController; + @Captor + private ArgumentCaptor intentCaptor; + @Captor + private ArgumentCaptor uriCaptor; + + private Activity callerActivity; + private AuthenticationActivityMock activity; + private ShadowActivity activityShadow; + private ActivityController activityController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + callerActivity = spy(Robolectric.buildActivity(Activity.class).get()); + } + + private void createActivity(Intent configurationIntent) { + activityController = Robolectric.buildActivity(AuthenticationActivityMock.class, configurationIntent); + activity = activityController.get(); + activity.setCustomTabsController(customTabsController); + activityShadow = shadowOf(activity); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldAuthenticateUsingBrowser() throws Exception { + AuthenticationActivity.authenticateUsingBrowser(callerActivity, uri); + verify(callerActivity).startActivity(intentCaptor.capture()); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + + verify(customTabsController).bindService(); + verify(customTabsController).launchUri(uriCaptor.capture()); + assertThat(uriCaptor.getValue(), is(notNullValue())); + assertThat(uriCaptor.getValue(), is(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + activityController.pause().stop(); + //Browser is shown + + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + activityController.newIntent(authenticationResultIntent); + activityController.start().resume(); + + assertThat(activity.getDeliveredIntent(), is(notNullValue())); + assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); + + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + verify(customTabsController).unbindService(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldAuthenticateAfterRecreatedUsingBrowser() throws Exception { + AuthenticationActivity.authenticateUsingBrowser(callerActivity, uri); + verify(callerActivity).startActivity(intentCaptor.capture()); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + + verify(customTabsController).bindService(); + verify(customTabsController).launchUri(uriCaptor.capture()); + assertThat(uriCaptor.getValue(), is(notNullValue())); + assertThat(uriCaptor.getValue(), is(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + //Browser is shown + //Memory needed. Let's kill the activity + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + recreateAndCallNewIntent(authenticationResultIntent); + + assertThat(activity.getDeliveredIntent(), is(notNullValue())); + assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); + + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + verify(customTabsController).unbindService(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldCancelAuthenticationUsingBrowser() throws Exception { + AuthenticationActivity.authenticateUsingBrowser(callerActivity, uri); + verify(callerActivity).startActivity(intentCaptor.capture()); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + + verify(customTabsController).bindService(); + verify(customTabsController).launchUri(uriCaptor.capture()); + assertThat(uriCaptor.getValue(), is(notNullValue())); + assertThat(uriCaptor.getValue(), is(uri)); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + activityController.pause().stop(); + //Browser is shown + + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(null); + activityController.newIntent(authenticationResultIntent); + activityController.start().resume(); + + assertThat(activity.getDeliveredIntent(), is(nullValue())); + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + verify(customTabsController).unbindService(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldAuthenticateUsingWebView() throws Exception { + verifyNoMoreInteractions(customTabsController); + + AuthenticationActivity.authenticateUsingWebView(callerActivity, uri, 123, "facebook", true); + verify(callerActivity).startActivityForResult(intentCaptor.capture(), eq(123)); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + final ShadowActivity.IntentForResult webViewIntent = activityShadow.getNextStartedActivityForResult(); + + Bundle extras = webViewIntent.intent.getExtras(); + assertThat(extras.containsKey(WebAuthActivity.CONNECTION_NAME_EXTRA), is(true)); + assertThat(extras.getString(WebAuthActivity.CONNECTION_NAME_EXTRA), is("facebook")); + assertThat(extras.containsKey(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + assertThat(extras.getBoolean(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + + assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); + assertThat(webViewIntent.intent, hasData(uri)); + assertThat(webViewIntent.requestCode, is(greaterThan(0))); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + activityController.pause(); + //WebViewActivity is shown + + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + activityShadow.receiveResult(webViewIntent.intent, Activity.RESULT_OK, authenticationResultIntent); + + assertThat(activity.getDeliveredIntent(), is(notNullValue())); + assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); + + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldAuthenticateAfterRecreatedUsingWebView() throws Exception { + verifyNoMoreInteractions(customTabsController); + + AuthenticationActivity.authenticateUsingWebView(callerActivity, uri, 123, "facebook", true); + verify(callerActivity).startActivityForResult(intentCaptor.capture(), eq(123)); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + final ShadowActivity.IntentForResult webViewIntent = activityShadow.getNextStartedActivityForResult(); + + Bundle extras = webViewIntent.intent.getExtras(); + assertThat(extras.containsKey(WebAuthActivity.CONNECTION_NAME_EXTRA), is(true)); + assertThat(extras.getString(WebAuthActivity.CONNECTION_NAME_EXTRA), is("facebook")); + assertThat(extras.containsKey(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + assertThat(extras.getBoolean(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + + assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); + assertThat(webViewIntent.intent, hasData(uri)); + assertThat(webViewIntent.requestCode, is(greaterThan(0))); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + //WebViewActivity is shown + //Memory needed. Let's kill the activity + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + recreateAndCallActivityResult(123, authenticationResultIntent); + + assertThat(activity.getDeliveredIntent(), is(notNullValue())); + assertThat(activity.getDeliveredIntent().getData(), is(resultUri)); + + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldCancelAuthenticationUsingWebView() throws Exception { + verifyNoMoreInteractions(customTabsController); + + AuthenticationActivity.authenticateUsingWebView(callerActivity, uri, 123, "facebook", true); + verify(callerActivity).startActivityForResult(intentCaptor.capture(), eq(123)); + + createActivity(intentCaptor.getValue()); + activityController.create().start().resume(); + final ShadowActivity.IntentForResult webViewIntent = activityShadow.getNextStartedActivityForResult(); + + Bundle extras = webViewIntent.intent.getExtras(); + assertThat(extras.containsKey(WebAuthActivity.CONNECTION_NAME_EXTRA), is(true)); + assertThat(extras.getString(WebAuthActivity.CONNECTION_NAME_EXTRA), is("facebook")); + assertThat(extras.containsKey(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + assertThat(extras.getBoolean(WebAuthActivity.FULLSCREEN_EXTRA), is(true)); + + assertThat(webViewIntent.intent, hasComponent(WebAuthActivity.class.getName())); + assertThat(webViewIntent.intent, hasData(uri)); + assertThat(webViewIntent.requestCode, is(greaterThan(0))); + assertThat(activity.getDeliveredIntent(), is(nullValue())); + activityController.pause().stop(); + //WebViewActivity is shown + + Intent authenticationResultIntent = new Intent(); + authenticationResultIntent.setData(resultUri); + activityShadow.receiveResult(webViewIntent.intent, Activity.RESULT_CANCELED, authenticationResultIntent); + + assertThat(activity.getDeliveredIntent(), is(nullValue())); + assertThat(activity.isFinishing(), is(true)); + + activityController.destroy(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldLaunchForBrowserAuthentication() throws Exception { + AuthenticationActivity.authenticateUsingBrowser(callerActivity, uri); + verify(callerActivity).startActivity(intentCaptor.capture()); + + Intent intent = intentCaptor.getValue(); + Assert.assertThat(intent, is(notNullValue())); + Assert.assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + Assert.assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + Assert.assertThat(intent, hasData(uri)); + + Bundle extras = intent.getExtras(); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(false)); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(false)); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + Assert.assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldLaunchForWebViewAuthentication() throws Exception { + AuthenticationActivity.authenticateUsingWebView(callerActivity, uri, 123, "facebook", true); + verify(callerActivity).startActivityForResult(intentCaptor.capture(), eq(123)); + + Intent intent = intentCaptor.getValue(); + Assert.assertThat(intent, is(notNullValue())); + Assert.assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + Assert.assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + Assert.assertThat(intent, hasData(uri)); + + Bundle extras = intentCaptor.getValue().getExtras(); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(true)); + Assert.assertThat(extras.getString(AuthenticationActivity.EXTRA_CONNECTION_NAME), is("facebook")); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + Assert.assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + Assert.assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + Assert.assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(false)); + } + + @Test + public void shouldCreateCustomTabsController() throws Exception { + final AuthenticationActivity authenticationActivity = new AuthenticationActivity(); + final CustomTabsController controller = authenticationActivity.createCustomTabsController(RuntimeEnvironment.application); + + assertThat(controller, is(notNullValue())); + } + + private void recreateAndCallNewIntent(Intent data) { + Bundle outState = new Bundle(); + activityController.saveInstanceState(outState); + activityController.pause().stop().destroy(); + createActivity(null); + activityController.create(outState).start().restoreInstanceState(outState); + activityController.newIntent(data); + activityController.resume(); + } + + private void recreateAndCallActivityResult(int reqCode, Intent data) { + Bundle outState = new Bundle(); + activityController.saveInstanceState(outState); + activityController.pause().stop().destroy(); + createActivity(null); + activityController.create(outState).start().restoreInstanceState(outState); + activity.onActivityResult(reqCode, Activity.RESULT_OK, data); + activityController.resume(); + } +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java new file mode 100644 index 000000000..ba83f7d13 --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java @@ -0,0 +1,255 @@ +package com.auth0.android.provider; + +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.support.customtabs.CustomTabsCallback; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.internal.verification.VerificationModeFactory; +import org.mockito.verification.Timeout; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = com.auth0.android.auth0.BuildConfig.class, sdk = 21, manifest = Config.NONE) +public class CustomTabsControllerTest { + + private static final String DEFAULT_BROWSER_PACKAGE = "com.auth0.browser"; + private static final String CHROME_STABLE_PACKAGE = "com.android.chrome"; + private static final String CHROME_SYSTEM_PACKAGE = "com.google.android.apps.chrome"; + private static final String CHROME_BETA_PACKAGE = "com.android.chrome.beta"; + private static final String CHROME_DEV_PACKAGE = "com.android.chrome.dev"; + private static final String CUSTOM_TABS_BROWSER_1 = "com.browser.customtabs1"; + private static final String CUSTOM_TABS_BROWSER_2 = "com.browser.customtabs2"; + private static final long MAX_TEST_WAIT_TIME_MS = 2000; + + @Mock + private Context context; + @Mock + private Uri uri; + @Mock + private CustomTabsClient customTabsClient; + @Captor + private ArgumentCaptor launchIntentCaptor; + @Captor + private ArgumentCaptor serviceIntentCaptor; + @Captor + private ArgumentCaptor serviceConnectionCaptor; + @Rule + public ExpectedException exception = ExpectedException.none(); + + private CustomTabsController controller; + + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + controller = new CustomTabsController(context, DEFAULT_BROWSER_PACKAGE); + } + + @Test + public void shouldChooseNullBrowserIfNoBrowserAvailable() throws Exception { + preparePackageManagerForCustomTabs(null); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(nullValue())); + } + + @Test + public void shouldChooseDefaultBrowserIfIsCustomTabsCapable() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, DEFAULT_BROWSER_PACKAGE); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(DEFAULT_BROWSER_PACKAGE)); + } + + @Test + public void shouldChooseDefaultBrowserIfNoOtherBrowserIsCustomTabsCapable() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(DEFAULT_BROWSER_PACKAGE)); + } + + @Test + public void shouldChooseChromeStableOverOtherCustomTabsCapableBrowsers() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CHROME_STABLE_PACKAGE, CHROME_SYSTEM_PACKAGE, CHROME_BETA_PACKAGE, CHROME_DEV_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CHROME_STABLE_PACKAGE)); + } + + @Test + public void shouldChooseChromeSystemOverOtherCustomTabsCapableBrowsers() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CHROME_SYSTEM_PACKAGE, CHROME_BETA_PACKAGE, CHROME_DEV_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CHROME_SYSTEM_PACKAGE)); + } + + @Test + public void shouldChooseChromeBetaOverOtherCustomTabsCapableBrowsers() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CHROME_BETA_PACKAGE, CHROME_DEV_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CHROME_BETA_PACKAGE)); + } + + @Test + public void shouldChooseChromeDevOverOtherCustomTabsCapableBrowsers() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CHROME_DEV_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CHROME_DEV_PACKAGE)); + } + + @Test + public void shouldChooseCustomTabsCapableBrowserIfAvailable() throws Exception { + preparePackageManagerForCustomTabs(DEFAULT_BROWSER_PACKAGE, CUSTOM_TABS_BROWSER_1, CUSTOM_TABS_BROWSER_2); + String bestPackage = CustomTabsController.getBestBrowserPackage(context); + assertThat(bestPackage, is(CUSTOM_TABS_BROWSER_1)); + } + + @Test + public void shouldUnbind() throws Exception { + bindService(true); + connectBoundService(); + + controller.unbindService(); + verify(context).unbindService(serviceConnectionCaptor.capture()); + final CustomTabsServiceConnection connection = serviceConnectionCaptor.getValue(); + CustomTabsServiceConnection controllerConnection = controller; + assertThat(connection, is(equalTo(controllerConnection))); + } + + @Test + public void shouldBindAndLaunchUri() throws Exception { + bindService(true); + controller.launchUri(uri); + connectBoundService(); + + verify(context, timeout(MAX_TEST_WAIT_TIME_MS)).startActivity(launchIntentCaptor.capture()); + Intent intent = launchIntentCaptor.getValue(); + assertThat(intent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION), is(true)); + assertThat(intent.getData(), is(uri)); + assertThat(intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + } + + @Test + public void shouldFailToBindButLaunchUri() throws Exception { + bindService(false); + controller.launchUri(uri); + + verify(context, timeout(MAX_TEST_WAIT_TIME_MS)).startActivity(launchIntentCaptor.capture()); + Intent intent = launchIntentCaptor.getValue(); + assertThat(intent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(intent.getData(), is(uri)); + assertThat(intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + } + + @Test + public void shouldNotLaunchUriIfContextNoLongerValid() throws Exception { + bindService(true); + controller.clearContext(); + controller.launchUri(uri); + verify(context, never()).startActivity(any(Intent.class)); + } + + @Test + public void shouldLaunchUriWithFallbackIfCustomTabIntentFails() throws Exception { + doThrow(ActivityNotFoundException.class) + .doNothing() + .when(context).startActivity(any(Intent.class)); + controller.launchUri(uri); + + verify(context, new Timeout(MAX_TEST_WAIT_TIME_MS, VerificationModeFactory.times(2))).startActivity(launchIntentCaptor.capture()); + List intents = launchIntentCaptor.getAllValues(); + + Intent customTabIntent = intents.get(0); + assertThat(customTabIntent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(customTabIntent.getData(), is(uri)); + assertThat(customTabIntent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + assertThat(customTabIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION), is(true)); + + Intent fallbackIntent = intents.get(1); + assertThat(fallbackIntent.getAction(), is(Intent.ACTION_VIEW)); + assertThat(fallbackIntent.getData(), is(uri)); + assertThat(fallbackIntent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY, is(Intent.FLAG_ACTIVITY_NO_HISTORY)); + assertThat(fallbackIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION), is(false)); + } + + //Helper Methods + + @SuppressWarnings("WrongConstant") + private void bindService(boolean willSucceed) { + Mockito.doReturn(willSucceed).when(context).bindService( + serviceIntentCaptor.capture(), + serviceConnectionCaptor.capture(), + Mockito.anyInt()); + controller.bindService(); + Intent intent = serviceIntentCaptor.getValue(); + assertThat(intent.getPackage(), is(DEFAULT_BROWSER_PACKAGE)); + } + + private void connectBoundService() { + CustomTabsServiceConnection conn = serviceConnectionCaptor.getValue(); + conn.onCustomTabsServiceConnected(new ComponentName(DEFAULT_BROWSER_PACKAGE, DEFAULT_BROWSER_PACKAGE + ".CustomTabsService"), customTabsClient); + verify(customTabsClient).newSession(Matchers.eq(null)); + verify(customTabsClient).warmup(eq(0L)); + } + + @SuppressWarnings("WrongConstant") + private void preparePackageManagerForCustomTabs(String defaultBrowserPackage, String... customTabEnabledPackages) { + PackageManager pm = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(pm); + ResolveInfo defaultPackage = resolveInfoForPackageName(defaultBrowserPackage); + when(pm.resolveActivity(any(Intent.class), anyInt())).thenReturn(defaultPackage); + when(pm.resolveService(any(Intent.class), eq(0))).thenReturn(defaultPackage); + + List customTabsCapable = new ArrayList<>(); + for (String customTabEnabledPackage : customTabEnabledPackages) { + customTabsCapable.add(resolveInfoForPackageName(customTabEnabledPackage)); + } + when(pm.queryIntentActivities(any(Intent.class), eq(0))).thenReturn(customTabsCapable); + } + + private ResolveInfo resolveInfoForPackageName(String packageName) { + if (packageName == null) { + return null; + } + ResolveInfo resInfo = mock(ResolveInfo.class); + resInfo.activityInfo = new ActivityInfo(); + resInfo.activityInfo.packageName = packageName; + return resInfo; + } +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java index 35a39dfb4..133bf8b96 100644 --- a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java @@ -17,7 +17,6 @@ import java.util.Date; import java.util.HashMap; -import static org.cyberneko.html.HTMLElements.HEAD; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; diff --git a/auth0/src/test/java/com/auth0/android/provider/RedirectActivityTest.java b/auth0/src/test/java/com/auth0/android/provider/RedirectActivityTest.java new file mode 100644 index 000000000..655d567ec --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/provider/RedirectActivityTest.java @@ -0,0 +1,86 @@ +package com.auth0.android.provider; + +import android.content.Intent; +import android.net.Uri; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivity; +import org.robolectric.util.ActivityController; + +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasData; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasFlags; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.robolectric.Shadows.shadowOf; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = com.auth0.android.auth0.BuildConfig.class, sdk = 18, manifest = Config.NONE) +public class RedirectActivityTest { + + + @Mock + private Uri uri; + + private RedirectActivity activity; + private ShadowActivity activityShadow; + private ActivityController activityController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + private void createActivity(Intent launchIntent) { + activityController = Robolectric.buildActivity(RedirectActivity.class, launchIntent); + activity = activityController.get(); + activityShadow = shadowOf(activity); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldLaunchAuthenticationActivityWithDataOnSuccess() throws Exception { + Intent resultIntent = new Intent(); + resultIntent.setData(uri); + + createActivity(resultIntent); + activityController.create().start().resume(); + + Intent authenticationIntent = activityShadow.getNextStartedActivity(); + assertThat(authenticationIntent, is(notNullValue())); + assertThat(authenticationIntent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(authenticationIntent, hasData(uri)); + assertThat(authenticationIntent, hasFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)); + + assertThat(activity.isFinishing(), is(true)); + activityController.destroy(); + } + + @SuppressWarnings("deprecation") + @Test + public void shouldLaunchAuthenticationActivityWithoutDataOnCancel() throws Exception { + Intent resultIntent = new Intent(); + resultIntent.setData(null); + + createActivity(resultIntent); + activityController.create().start().resume(); + + Intent authenticationIntent = activityShadow.getNextStartedActivity(); + assertThat(authenticationIntent, is(notNullValue())); + assertThat(authenticationIntent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(authenticationIntent.getData(), is(nullValue())); + assertThat(authenticationIntent, hasFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)); + + assertThat(activity.isFinishing(), is(true)); + activityController.destroy(); + } +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java index 752ca32aa..4a9e7103c 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java @@ -2,11 +2,12 @@ import android.app.Activity; import android.app.Dialog; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.content.res.Resources; import android.net.Uri; +import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Base64; @@ -36,9 +37,7 @@ import java.util.Map; import java.util.Set; -import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; -import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasFlag; import static android.support.test.espresso.intent.matcher.UriMatchers.hasHost; import static android.support.test.espresso.intent.matcher.UriMatchers.hasParamWithName; @@ -56,9 +55,10 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -91,6 +91,10 @@ public void setUp() throws Exception { MockitoAnnotations.initMocks(this); activity = spy(Robolectric.buildActivity(Activity.class).get()); account = new Auth0("clientId", "domain"); + + //Next line is needed to avoid CustomTabService from being bound to Test environment + //noinspection WrongConstant + doReturn(false).when(activity).bindService(any(Intent.class), any(ServiceConnection.class), anyInt()); } @SuppressWarnings("deprecation") @@ -946,56 +950,55 @@ public void shouldBuildAuthorizeURIWithResponseTypeCode() throws Exception { @SuppressWarnings("deprecation") @Test public void shouldStartWithBrowser() throws Exception { - Activity activity = mock(Activity.class); - Context appContext = mock(Context.class); - when(activity.getApplicationContext()).thenReturn(appContext); - when(activity.getPackageName()).thenReturn("package"); - when(appContext.getPackageName()).thenReturn("package"); WebAuthProvider.init(account) .useBrowser(true) .useCodeGrant(false) .start(activity, callback); - ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); verify(activity).startActivity(intentCaptor.capture()); - assertThat(intentCaptor.getValue(), is(notNullValue())); - assertThat(intentCaptor.getValue(), hasAction(Intent.ACTION_VIEW)); - assertThat(intentCaptor.getValue(), hasFlag(Intent.FLAG_ACTIVITY_NO_HISTORY)); + Intent intent = intentCaptor.getValue(); + assertThat(intent, is(notNullValue())); + assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + assertThat(intent.getData(), is(notNullValue())); + + Bundle extras = intentCaptor.getValue().getExtras(); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(false)); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(false)); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); } @SuppressWarnings("deprecation") @Test public void shouldStartWithWebViewAndDefaultConnection() throws Exception { - Activity activity = mock(Activity.class); - Context appContext = mock(Context.class); - when(activity.getApplicationContext()).thenReturn(appContext); - when(activity.getPackageName()).thenReturn("package"); - when(appContext.getPackageName()).thenReturn("package"); WebAuthProvider.init(account) .useBrowser(false) .useCodeGrant(false) .useFullscreen(false) .start(activity, callback, REQUEST_CODE); - ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); verify(activity).startActivityForResult(intentCaptor.capture(), any(Integer.class)); - ComponentName expComponent = new ComponentName("package", WebAuthActivity.class.getName()); - assertThat(intentCaptor.getValue(), is(notNullValue())); - assertThat(intentCaptor.getValue(), hasComponent(expComponent)); - assertThat(intentCaptor.getValue(), hasExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, null)); - assertThat(intentCaptor.getValue(), hasExtra(WebAuthActivity.FULLSCREEN_EXTRA, false)); + Intent intent = intentCaptor.getValue(); + assertThat(intent, is(notNullValue())); + assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + assertThat(intent.getData(), is(notNullValue())); + + Bundle extras = intentCaptor.getValue().getExtras(); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(true)); + assertThat(extras.getString(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(nullValue())); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(false)); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(false)); } @SuppressWarnings("deprecation") @Test public void shouldStartWithWebViewAndCustomConnection() throws Exception { - Activity activity = mock(Activity.class); - Context appContext = mock(Context.class); - when(activity.getApplicationContext()).thenReturn(appContext); - when(activity.getPackageName()).thenReturn("package"); - when(appContext.getPackageName()).thenReturn("package"); WebAuthProvider.init(account) .useBrowser(false) .withConnection("my-connection") @@ -1003,14 +1006,21 @@ public void shouldStartWithWebViewAndCustomConnection() throws Exception { .useFullscreen(true) .start(activity, callback); - ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); verify(activity).startActivityForResult(intentCaptor.capture(), any(Integer.class)); - ComponentName expComponent = new ComponentName("package", WebAuthActivity.class.getName()); - assertThat(intentCaptor.getValue(), is(notNullValue())); - assertThat(intentCaptor.getValue(), hasComponent(expComponent)); - assertThat(intentCaptor.getValue(), hasExtra(WebAuthActivity.CONNECTION_NAME_EXTRA, "my-connection")); - assertThat(intentCaptor.getValue(), hasExtra(WebAuthActivity.FULLSCREEN_EXTRA, true)); + Intent intent = intentCaptor.getValue(); + assertThat(intent, is(notNullValue())); + assertThat(intent, hasComponent(AuthenticationActivity.class.getName())); + assertThat(intent, hasFlag(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + assertThat(intent.getData(), is(notNullValue())); + + Bundle extras = intent.getExtras(); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_CONNECTION_NAME), is(true)); + assertThat(extras.getString(AuthenticationActivity.EXTRA_CONNECTION_NAME), is("my-connection")); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_FULL_SCREEN), is(true)); + assertThat(extras.containsKey(AuthenticationActivity.EXTRA_USE_BROWSER), is(true)); + assertThat(extras.getBoolean(AuthenticationActivity.EXTRA_USE_BROWSER), is(false)); } @SuppressWarnings({"deprecation", "ThrowableResultOfMethodCallIgnored"}) @@ -1070,6 +1080,7 @@ public void shouldResumeWithIntentWithResponseTypeIdToken() throws Exception { Intent intent = createAuthIntent(createHash(customNonceJWT(sentNonce), null, null, null, null, sentState, null, null)); assertTrue(WebAuthProvider.resume(intent)); + verify(callback).onSuccess(any(Credentials.class)); } @@ -1119,7 +1130,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { String sentState = uri.getQueryParameter(KEY_STATE); assertThat(sentState, is(not(isEmptyOrNullString()))); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, sentState, null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, sentState, null, null)); assertTrue(WebAuthProvider.resume(intent)); ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); @@ -1158,7 +1169,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { String sentState = uri.getQueryParameter(KEY_STATE); assertThat(sentState, is(not(isEmptyOrNullString()))); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, sentState, null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, sentState, null, null)); assertTrue(WebAuthProvider.resume(REQUEST_CODE, Activity.RESULT_OK, intent)); ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); @@ -1186,10 +1197,18 @@ public void shouldResumeWithIntentWithImplicitGrant() throws Exception { String sentState = uri.getQueryParameter(KEY_STATE); assertThat(sentState, is(not(isEmptyOrNullString()))); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, sentState, null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, sentState, null, null)); assertTrue(WebAuthProvider.resume(intent)); - verify(callback).onSuccess(any(Credentials.class)); + ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); + verify(callback).onSuccess(credentialsCaptor.capture()); + + assertThat(credentialsCaptor.getValue(), is(notNullValue())); + assertThat(credentialsCaptor.getValue().getIdToken(), is("urlId")); + assertThat(credentialsCaptor.getValue().getAccessToken(), is("urlAccess")); + assertThat(credentialsCaptor.getValue().getRefreshToken(), is("urlRefresh")); + assertThat(credentialsCaptor.getValue().getType(), is("urlType")); + assertThat(credentialsCaptor.getValue().getExpiresIn(), is(1111L)); } @SuppressWarnings("deprecation") @@ -1205,10 +1224,18 @@ public void shouldResumeWithRequestCodeWithImplicitGrant() throws Exception { String sentState = uri.getQueryParameter(KEY_STATE); assertThat(sentState, is(not(isEmptyOrNullString()))); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, sentState, null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, sentState, null, null)); assertTrue(WebAuthProvider.resume(REQUEST_CODE, Activity.RESULT_OK, intent)); - verify(callback).onSuccess(any(Credentials.class)); + ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); + verify(callback).onSuccess(credentialsCaptor.capture()); + + assertThat(credentialsCaptor.getValue(), is(notNullValue())); + assertThat(credentialsCaptor.getValue().getIdToken(), is("urlId")); + assertThat(credentialsCaptor.getValue().getAccessToken(), is("urlAccess")); + assertThat(credentialsCaptor.getValue().getRefreshToken(), is("urlRefresh")); + assertThat(credentialsCaptor.getValue().getType(), is("urlType")); + assertThat(credentialsCaptor.getValue().getExpiresIn(), is(1111L)); } @Test @@ -1253,7 +1280,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { .useCodeGrant(true) .withPKCE(pkce) .start(activity, callback); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, "1234567890", null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, "1234567890", null, null)); assertTrue(WebAuthProvider.resume(intent)); verify(callback).onFailure(dialog); @@ -1276,7 +1303,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { .useCodeGrant(true) .withPKCE(pkce) .start(activity, callback); - Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", null, "1234567890", null, null)); + Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", 1111L, "1234567890", null, null)); assertTrue(WebAuthProvider.resume(intent)); verify(callback).onFailure(exception);