From 4555ca3264f2d1b862fcb6a526de5503cd57d198 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Wed, 6 Dec 2017 17:10:53 -0800 Subject: [PATCH 01/19] First commit using chrometabs --- CHANGELOG.md | 5 + README.md | 32 +++ core-android/build.gradle | 1 + .../uber/sdk/android/core/auth/AuthUtils.java | 96 ++++++++- .../android/core/auth/CustomTabsHelper.java | 190 ++++++++++++++++++ .../sdk/android/core/auth/LoginActivity.java | 54 +---- .../sdk/android/core/auth/LoginManager.java | 84 +++++++- .../uber/sdk/android/core/utils/Utility.java | 13 ++ .../sdk/android/core/auth/AuthUtilsTest.java | 20 +- gradle/dependencies.gradle | 3 +- .../login-sample/src/main/AndroidManifest.xml | 8 + .../android/samples/LoginSampleActivity.java | 6 + .../src/main/AndroidManifest.xml | 7 + 13 files changed, 449 insertions(+), 70 deletions(-) create mode 100644 core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a5fb2bef..ac965f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ v0.8.0 - TBD ------------ + ### Changed - [Issue #101](https://github.com/uber/rides-android-sdk/issues/101) LoginManager now uses AccessTokenStorage +### Added + - [Issue #22](https://github.com/uber/rides-android-sdk/issues/22) Customtab support + + v0.7.0 - 11/17/2017 ------------ diff --git a/README.md b/README.md index 5135edd7..d26176a2 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ To get the hash of your signing certificate, run this command with the alias of keytool -exportcert -alias -keystore | openssl sha1 -binary | openssl base64 ``` + Before you can request any rides, you need to get an `AccessToken`. The Uber Rides SDK provides the `LoginManager` class for this task. Simply create a new instance and use its login method to present the login screen to the user. ```java @@ -199,6 +200,37 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data){ } ``` +#### Authentication Migration (Version 0.8 and above) + +We are moving the startActivityForResult/onActivityResult contract to use the standard URI +contract indicated in the [IETF RFC](https://tools.ietf.org/html/draft-ietf-oauth-native-apps-12). + +Now, you must additionally specify your redirect_uri in the Android Manifest and +proxy that information to the Uber SDK. The recommended format is com.example.yourpackage://redirect-uri + +To handle migrations from older Uber apps using the old contract, we recommend you implement both +approaches in the +short term until we indicate otherwise. + +```xml + + + + + +``` + +Following this change, you must proxy the results to the `LoginManager` from the called activity. + +```java +@Override +protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + loginManager.handleAuthorizationResult(intent); +} +``` + The default behavior of calling `LoginManager.login(activity)` is to activate Single Sign On, and if that is unavailable, fallback to Implicit Grant if privileged scopes are not requested, otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager.setRedirectForAuthorizationCode(true)` to prevent the redirect to the Play Store. Implicit Grant will allow access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes). #### Login Errors diff --git a/core-android/build.gradle b/core-android/build.gradle index 0c1bdd09..955dbf24 100644 --- a/core-android/build.gradle +++ b/core-android/build.gradle @@ -49,6 +49,7 @@ dependencies { compile (deps.uber.uberCore) { exclude module: 'slf4j-log4j12' } + compile deps.support.chrometabs compile deps.misc.jsr305 compile deps.support.appCompat compile deps.support.annotations diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/AuthUtils.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/AuthUtils.java index 6ff243c8..b7e539b1 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/AuthUtils.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/AuthUtils.java @@ -22,17 +22,25 @@ package com.uber.sdk.android.core.auth; +import android.app.Activity; import android.content.Intent; +import android.content.pm.ResolveInfo; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Base64; +import android.webkit.WebView; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.Scope; +import com.uber.sdk.core.client.SessionConfiguration; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; +import java.util.Locale; import java.util.Set; /** @@ -118,6 +126,22 @@ static Collection stringCollectionToScopeCollection(@NonNull Collection resolvedActivityList = activity.getPackageManager() + .queryIntentActivities(activityIntent, 0); + + boolean supported = false; + for (ResolveInfo resolveInfo : resolvedActivityList) { + if (resolveInfo.activityInfo.packageName.equals(activity.getPackageName())) { + supported = true; + } + } + return supported; + } + /** * Converts a {@link Collection} of {@link Scope}s into a space-delimited {@link String}. * @@ -150,7 +174,29 @@ public static String mergeScopeStrings(String... scopes) { } @NonNull - static Intent parseTokenUri(@NonNull Uri uri) throws LoginAuthenticationException { + static AccessToken parseTokenUri(@NonNull Uri uri) throws LoginAuthenticationException { + final long expiresIn; + try { + expiresIn = Long.valueOf(uri.getQueryParameter(KEY_EXPIRATION_TIME)); + } catch (NumberFormatException ex) { + throw new LoginAuthenticationException(AuthenticationError.INVALID_RESPONSE); + } + + final String accessToken = uri.getQueryParameter(KEY_TOKEN); + final String refreshToken = uri.getQueryParameter(KEY_REFRESH_TOKEN); + final String scope = uri.getQueryParameter(KEY_SCOPES); + final String tokenType = uri.getQueryParameter(KEY_TOKEN_TYPE); + + if (TextUtils.isEmpty(accessToken) || TextUtils.isEmpty(scope) || TextUtils.isEmpty(tokenType)) { + throw new LoginAuthenticationException(AuthenticationError.INVALID_RESPONSE); + } + + return new AccessToken(expiresIn, AuthUtils.stringToScopeCollection + (scope), accessToken, refreshToken, tokenType); + } + + @NonNull + static Intent parseTokenUriToIntent(@NonNull Uri uri) throws LoginAuthenticationException { final long expiresIn; try { expiresIn = Long.valueOf(uri.getQueryParameter(KEY_EXPIRATION_TIME)); @@ -201,4 +247,52 @@ static AccessToken createAccessToken(Intent intent) { static String createEncodedParam(String rawParam) { return Base64.encodeToString(rawParam.getBytes(), Base64.DEFAULT); } + + /** + * Builds a URL {@link String} using the necessary parameters to load in the {@link WebView}. + * + * @return the URL to load in the {@link WebView} + */ + @NonNull + static String buildUrl( + @NonNull String redirectUri, + @NonNull ResponseType responseType, + @NonNull SessionConfiguration configuration) { + + final String CLIENT_ID_PARAM = "client_id"; + final String ENDPOINT = "login"; + final String HTTPS = "https"; + final String PATH = "oauth/v2/authorize"; + final String REDIRECT_PARAM = "redirect_uri"; + final String RESPONSE_TYPE_PARAM = "response_type"; + final String SCOPE_PARAM = "scope"; + final String SHOW_FB_PARAM = "show_fb"; + final String SIGNUP_PARAMS = "signup_params"; + final String REDIRECT_LOGIN = "{\"redirect_to_login\":true}"; + + + + Uri.Builder builder = new Uri.Builder(); + builder.scheme(HTTPS) + .authority(ENDPOINT + "." + configuration.getEndpointRegion().getDomain()) + .appendEncodedPath(PATH) + .appendQueryParameter(CLIENT_ID_PARAM, configuration.getClientId()) + .appendQueryParameter(REDIRECT_PARAM, redirectUri) + .appendQueryParameter(RESPONSE_TYPE_PARAM, responseType.toString().toLowerCase( + Locale.US)) + .appendQueryParameter(SCOPE_PARAM, getScopes(configuration)) + .appendQueryParameter(SHOW_FB_PARAM, "false") + .appendQueryParameter(SIGNUP_PARAMS, AuthUtils.createEncodedParam(REDIRECT_LOGIN)); + + return builder.build().toString(); + } + + private static String getScopes(SessionConfiguration configuration) { + String scopes = AuthUtils.scopeCollectionToString(configuration.getScopes()); + if (!configuration.getCustomScopes().isEmpty()) { + scopes = AuthUtils.mergeScopeStrings(scopes, + AuthUtils.customScopeCollectionToString(configuration.getCustomScopes())); + } + return scopes; + } } diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java new file mode 100644 index 00000000..631a192e --- /dev/null +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java @@ -0,0 +1,190 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.uber.sdk.android.core.auth; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.customtabs.CustomTabsIntent; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for Custom Tabs. + */ +public class CustomTabsHelper { + private static final String TAG = "CustomTabsHelper"; + static final String STABLE_PACKAGE = "com.android.chrome"; + static final String BETA_PACKAGE = "com.chrome.beta"; + static final String DEV_PACKAGE = "com.chrome.dev"; + static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; + private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = + "android.support.customtabs.extra.KEEP_ALIVE"; + private static final String ACTION_CUSTOM_TABS_CONNECTION = + "android.support.customtabs.action.CustomTabsService"; + + private static String packageNameToUse; + + private CustomTabsHelper() {} + + /** + * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView. + * + * @param activity The host activity. + * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available. + * @param uri the Uri to be opened. + * @param fallback a CustomTabFallback to be used if Custom Tabs is not available. + */ + public static void openCustomTab(Activity activity, + CustomTabsIntent customTabsIntent, + Uri uri, + CustomTabFallback fallback) { + String packageName = getPackageNameToUse(activity); + + if (packageName == null) { + if (fallback != null) { + fallback.openUri(activity, uri); + } + } else { + customTabsIntent.intent.setPackage(packageName); + customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + customTabsIntent.launchUrl(activity, uri); + } + } + + /** + * Goes through all apps that handle VIEW intents and have a warmup service. Picks + * the one chosen by the user if there is one, otherwise makes a best effort to return a + * valid package name. + * + * This is not threadsafe. + * + * @param context {@link Context} to use for accessing {@link PackageManager}. + * @return The package name recommended to use for connecting to custom tabs related components. + */ + @Nullable + public static String getPackageNameToUse(Context context) { + if (packageNameToUse != null) return packageNameToUse; + + PackageManager pm = context.getPackageManager(); + // Get default VIEW intent handler. + Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); + String defaultViewHandlerPackageName = null; + if (defaultViewHandlerInfo != null) { + defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; + } + + // Get all apps that can handle VIEW intents. + List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); + List packagesSupportingCustomTabs = 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) { + packagesSupportingCustomTabs.add(info.activityInfo.packageName); + } + } + + // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents + // and service calls. + if (packagesSupportingCustomTabs.isEmpty()) { + packageNameToUse = null; + } else if (packagesSupportingCustomTabs.size() == 1) { + packageNameToUse = packagesSupportingCustomTabs.get(0); + } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) + && !hasSpecializedHandlerIntents(context, activityIntent) + && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { + packageNameToUse = defaultViewHandlerPackageName; + } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { + packageNameToUse = STABLE_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { + packageNameToUse = BETA_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { + packageNameToUse = DEV_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { + packageNameToUse = LOCAL_PACKAGE; + } + return packageNameToUse; + } + + /** + * Used to check whether there is a specialized handler for a given intent. + * @param intent The intent to check with. + * @return Whether there is a specialized handler for the given intent. + */ + private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) { + try { + PackageManager pm = context.getPackageManager(); + List handlers = pm.queryIntentActivities( + intent, + PackageManager.GET_RESOLVED_FILTER); + if (handlers == null || handlers.size() == 0) { + return false; + } + for (ResolveInfo resolveInfo : handlers) { + IntentFilter filter = resolveInfo.filter; + if (filter == null) continue; + if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue; + if (resolveInfo.activityInfo == null) continue; + return true; + } + } catch (RuntimeException e) { + Log.e(TAG, "Runtime exception while getting specialized handlers"); + } + return false; + } + + /** + * @return All possible chrome package names that provide custom tabs feature. + */ + public static String[] getPackages() { + return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; + } + + /** + * Fallback that uses browser + */ + static class BrowserFallback implements CustomTabFallback { + @Override + public void openUri(Activity activity, Uri uri) { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); + } + } + + /** + * To be used as a fallback to open the Uri when Custom Tabs is not available. + */ + interface CustomTabFallback { + /** + * + * @param activity The Activity that wants to open the Uri. + * @param uri The uri to be opened by the fallback. + */ + void openUri(Activity activity, Uri uri); + } +} \ No newline at end of file diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java index a4ee7d98..2d86caa0 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java @@ -39,8 +39,6 @@ import com.uber.sdk.android.core.R; import com.uber.sdk.core.client.SessionConfiguration; -import java.util.Locale; - /** * {@link android.app.Activity} that shows web view for Uber user authentication and authorization. */ @@ -108,7 +106,7 @@ protected void onCreate(Bundle savedInstanceState) { webView.getSettings().setAppCacheEnabled(true); webView.getSettings().setDomStorageEnabled(true); webView.setWebViewClient(createOAuthClient(redirectUri)); - webView.loadUrl(buildUrl(redirectUri, responseType, sessionConfiguration)); + webView.loadUrl(AuthUtils.buildUrl(redirectUri, responseType, sessionConfiguration)); } protected OAuthWebViewClient createOAuthClient(String redirectUri) { @@ -128,7 +126,7 @@ void onError(@NonNull AuthenticationError error) { void onTokenReceived(@NonNull Uri uri) { try { - Intent data = AuthUtils.parseTokenUri(uri); + Intent data = AuthUtils.parseTokenUriToIntent(uri); setResult(RESULT_OK, data); finish(); @@ -150,54 +148,6 @@ void onCodeReceived(Uri uri) { } } - /** - * Builds a URL {@link String} using the necessary parameters to load in the {@link WebView}. - * - * @return the URL to load in the {@link WebView} - */ - @NonNull - @VisibleForTesting - String buildUrl( - @NonNull String redirectUri, - @NonNull ResponseType responseType, - @NonNull SessionConfiguration configuration) { - - final String CLIENT_ID_PARAM = "client_id"; - final String ENDPOINT = "login"; - final String HTTPS = "https"; - final String PATH = "oauth/v2/authorize"; - final String REDIRECT_PARAM = "redirect_uri"; - final String RESPONSE_TYPE_PARAM = "response_type"; - final String SCOPE_PARAM = "scope"; - final String SHOW_FB_PARAM = "show_fb"; - final String SIGNUP_PARAMS = "signup_params"; - final String REDIRECT_LOGIN = "{\"redirect_to_login\":true}"; - - - - Uri.Builder builder = new Uri.Builder(); - builder.scheme(HTTPS) - .authority(ENDPOINT + "." + configuration.getEndpointRegion().getDomain()) - .appendEncodedPath(PATH) - .appendQueryParameter(CLIENT_ID_PARAM, configuration.getClientId()) - .appendQueryParameter(REDIRECT_PARAM, redirectUri) - .appendQueryParameter(RESPONSE_TYPE_PARAM, responseType.toString().toLowerCase(Locale.US)) - .appendQueryParameter(SCOPE_PARAM, getScopes(configuration)) - .appendQueryParameter(SHOW_FB_PARAM, "false") - .appendQueryParameter(SIGNUP_PARAMS, AuthUtils.createEncodedParam(REDIRECT_LOGIN)); - - return builder.build().toString(); - } - - private String getScopes(SessionConfiguration configuration) { - String scopes = AuthUtils.scopeCollectionToString(configuration.getScopes()); - if (!configuration.getCustomScopes().isEmpty()) { - scopes = AuthUtils.mergeScopeStrings(scopes, - AuthUtils.customScopeCollectionToString(configuration.getCustomScopes())); - } - return scopes; - } - /** * Custom {@link WebViewClient} for authorization. */ diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index af0e669e..d8cac4c4 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -24,14 +24,18 @@ import android.app.Activity; import android.content.Intent; +import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.customtabs.CustomTabsIntent; +import android.text.TextUtils; import android.util.Log; import com.uber.sdk.android.core.BuildConfig; import com.uber.sdk.android.core.UberSdk; import com.uber.sdk.android.core.install.SignupDeeplink; import com.uber.sdk.android.core.utils.AppProtocol; +import com.uber.sdk.android.core.utils.Utility; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.AccessTokenStorage; import com.uber.sdk.core.auth.Scope; @@ -54,6 +58,11 @@ public class LoginManager { */ static final String EXTRA_ERROR = "ERROR"; + /** + * Used to retrieve the {@link AuthenticationError} from a result URI + */ + static final String QUERY_PARAM_ERROR = "error"; + /** * Used to retrieve the Access Token from an {@link Intent}. */ @@ -145,6 +154,8 @@ public void login(@NonNull Activity activity) { checkNotEmpty(sessionConfiguration.getScopes(), "Scopes must be set in the Session " + "Configuration."); + validateRedirectUriRegistration(activity); + SsoDeeplink ssoDeeplink = new SsoDeeplink.Builder(activity) .clientId(sessionConfiguration.getClientId()) .scopes(sessionConfiguration.getScopes()) @@ -169,9 +180,7 @@ public void login(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForImplicitGrant(@NonNull Activity activity) { - - Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, ResponseType.TOKEN); - activity.startActivityForResult(intent, requestCode); + loginWithCustomtab(activity, ResponseType.TOKEN); } /** @@ -180,9 +189,7 @@ public void loginForImplicitGrant(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForAuthorizationCode(@NonNull Activity activity) { - - Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, ResponseType.CODE); - activity.startActivityForResult(intent, requestCode); + loginWithCustomtab(activity, ResponseType.CODE); } /** @@ -304,6 +311,58 @@ public void onActivityResult( } } + private void loginWithCustomtab(@NonNull final Activity activity, @NonNull ResponseType responseType) { + + final String url = AuthUtils.buildUrl(sessionConfiguration.getRedirectUri(), responseType, + sessionConfiguration); + + if (AuthUtils.isRedirectUriRegistered(activity, + Uri.parse(sessionConfiguration.getRedirectUri()))) { + final CustomTabsIntent intent = new CustomTabsIntent.Builder().build(); + + CustomTabsHelper.openCustomTab(activity, intent, Uri.parse(url), + new CustomTabsHelper.BrowserFallback()); + } else { + Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, responseType); + activity.startActivityForResult(intent, requestCode); + } + } + + /** + * Handle the Uber authorization result. + * This will parse the Intent to pull the access token or error out of the Data URI, and call + * the set callback. + * + * @param data + */ + public void handleAuthorizationResult(@NonNull Intent data) { + if (data.getData() != null + && data.getData().toString().startsWith(sessionConfiguration.getRedirectUri())) { + + final String fragment = data.getData().getFragment(); + + if (fragment == null) { + callback.onLoginError(AuthenticationError.INVALID_RESPONSE); + return; + } + + final Uri fragmentUri = new Uri.Builder().encodedQuery(fragment).build(); + + final String error = fragmentUri.getQueryParameter(QUERY_PARAM_ERROR); + if (!TextUtils.isEmpty(error)) { + callback.onLoginError(AuthenticationError.fromString(error)); + return; + } + + try { + AccessToken token = AuthUtils.parseTokenUri(fragmentUri); + callback.onLoginSuccess(token); + } catch (LoginAuthenticationException e) { + callback.onLoginError(e.getAuthenticationError()); + } + } + } + private void handleResultCancelled( @NonNull Activity activity, @Nullable Intent data) {// An error occurred during login @@ -363,4 +422,17 @@ private void handleResultOk(@Nullable Intent data) { } } + + private void validateRedirectUriRegistration(@NonNull Activity activity) { + if (!AuthUtils.isRedirectUriRegistered(activity, Uri.parse(sessionConfiguration + .getRedirectUri()))) { + + String error = "Must now register redirect_uri in AndroidManifest.xml. See README."; + if (Utility.isDebugable(activity)) { + throw new IllegalStateException(error); + } else { + Log.e("Uber SDK", error); + } + } + } } diff --git a/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java b/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java index f9e5996d..25d58c4c 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java @@ -1,5 +1,9 @@ package com.uber.sdk.android.core.utils; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.support.annotation.NonNull; + import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -15,6 +19,15 @@ public static String sha1hash(byte[] bytes) { return hashWithAlgorithm(HASH_ALGORITHM_SHA1, bytes); } + /** + * Detects if the Application is currently in a Debug state + */ + public static boolean isDebugable(@NonNull Context context) { + return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo + .FLAG_DEBUGGABLE ) ); + + } + private static String hashWithAlgorithm(String algorithm, String key) { return hashWithAlgorithm(algorithm, key.getBytes()); } diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/AuthUtilsTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/AuthUtilsTest.java index a261e186..69d0bc17 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/AuthUtilsTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/AuthUtilsTest.java @@ -117,7 +117,7 @@ public void scopeCollectionToString_whenEmptyScopeCollection_shouldReturnEmptySt public void generateAccessTokenFromUrl_whenNullFragment_shouldThrowInvalidResponseError() { String redirectUri = "http://localhost:1234/"; try { - AuthUtils.parseTokenUri(Uri.parse(redirectUri)); + AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUri)); fail("Should throw an exception"); } catch (LoginAuthenticationException e) { assertEquals(AuthenticationError.INVALID_RESPONSE, e.getAuthenticationError()); @@ -128,7 +128,7 @@ public void generateAccessTokenFromUrl_whenNullFragment_shouldThrowInvalidRespon public void generateAccessTokenFromUrl_whenValidErrorInQueryParameter_shouldThrowAuthenticationError() { String redirectUri = "http://localhost:1234?error=mismatching_redirect_uri"; try { - AuthUtils.parseTokenUri(Uri.parse(redirectUri)); + AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUri)); fail("Should throw an exception"); } catch (LoginAuthenticationException e) { assertEquals(AuthenticationError.INVALID_RESPONSE, e.getAuthenticationError()); @@ -139,7 +139,7 @@ public void generateAccessTokenFromUrl_whenValidErrorInQueryParameter_shouldThro public void generateAccessTokenFromUrl_whenInvalidErrorInQueryParameter_shouldThrowAuthenticationError() { String redirectUri = "http://localhost:1234?error=bogus_error"; try { - AuthUtils.parseTokenUri(Uri.parse(redirectUri)); + AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUri)); fail("Should throw an exception"); } catch (LoginAuthenticationException e) { assertEquals(AuthenticationError.INVALID_RESPONSE, e.getAuthenticationError()); @@ -150,7 +150,7 @@ public void generateAccessTokenFromUrl_whenInvalidErrorInQueryParameter_shouldTh public void generateAccessTokenFromUrl_whenNoToken_shouldThrowAuthenticationError() { String redirectUrl = "http://localhost:1234?expires_in=" + EXPIRATION_TIME + "&scope=history"; try { - AuthUtils.parseTokenUri(Uri.parse(redirectUrl)); + AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUrl)); fail("Should throw an exception"); } catch (LoginAuthenticationException e) { assertEquals(AuthenticationError.INVALID_RESPONSE, e.getAuthenticationError()); @@ -161,7 +161,7 @@ public void generateAccessTokenFromUrl_whenNoToken_shouldThrowAuthenticationErro public void generateAccessTokenFromUrl_whenNoExpirationTime_shouldThrowAuthenticationError() { String redirectUrl = "http://localhost:1234?access_token=" + ACCESS_TOKEN_STRING + "&scope=history"; try { - AuthUtils.parseTokenUri(Uri.parse(redirectUrl)); + AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUrl)); fail("Should throw an exception"); } catch (LoginAuthenticationException e) { assertEquals(AuthenticationError.INVALID_RESPONSE, e.getAuthenticationError()); @@ -173,7 +173,7 @@ public void generateAccessTokenFromUrl_whenNoScopes_shouldThrowAuthenticationErr String redirectUrl = "http://localhost:1234?access_token=" + ACCESS_TOKEN_STRING + "&expires_in=" + EXPIRATION_TIME; try { - AuthUtils.parseTokenUri(Uri.parse(redirectUrl)); + AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUrl)); fail("Should throw an exception"); } catch (LoginAuthenticationException e) { assertEquals(AuthenticationError.INVALID_RESPONSE, e.getAuthenticationError()); @@ -185,7 +185,7 @@ public void generateAccessTokenFromUrl_whenBadExpirationTime_shouldThrowAuthenti String redirectUrl = "http://localhost:1234?access_token=" + ACCESS_TOKEN_STRING + "&expires_in=notALong" + "&scope=history"; try { - AuthUtils.parseTokenUri(Uri.parse(redirectUrl)); + AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUrl)); fail("Should throw an exception"); } catch (LoginAuthenticationException e) { assertEquals(AuthenticationError.INVALID_RESPONSE, e.getAuthenticationError()); @@ -198,7 +198,7 @@ public void generateAccessTokenFromUrl_whenBadScopes_shouldThrowAuthenticationEr EXPIRATION_TIME + "&scope=history notAScopeAtAll"; try { - AuthUtils.parseTokenUri(Uri.parse(redirectUrl)); + AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUrl)); fail("Should throw an exception"); } catch (LoginAuthenticationException e) { assertEquals(AuthenticationError.INVALID_RESPONSE, e.getAuthenticationError()); @@ -211,7 +211,7 @@ public void generateAccessTokenFromUrl_whenValidAccessTokenWithOneScope_shouldGe String redirectUrl = "http://localhost:1234?access_token=" + ACCESS_TOKEN_STRING + "&expires_in=" + EXPIRATION_TIME + "&scope=history" + "&token_type=" + BEARER; - AccessToken accessToken = AuthUtils.createAccessToken(AuthUtils.parseTokenUri(Uri.parse(redirectUrl))); + AccessToken accessToken = AuthUtils.createAccessToken(AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUrl))); assertNotNull(accessToken); assertEquals(accessToken.getToken(), ACCESS_TOKEN_STRING); assertEquals(accessToken.getScopes().size(), 1); @@ -225,7 +225,7 @@ public void generateAccessTokenFromUrl_whenValidAccessTokenWithMultipleScopes_sh String redirectUrl = "http://localhost:1234?access_token=" + ACCESS_TOKEN_STRING + "&expires_in=" + EXPIRATION_TIME + "&scope=history profile" + "&token_type=" + BEARER; - AccessToken accessToken = AuthUtils.createAccessToken(AuthUtils.parseTokenUri(Uri.parse(redirectUrl))); + AccessToken accessToken = AuthUtils.createAccessToken(AuthUtils.parseTokenUriToIntent(Uri.parse(redirectUrl))); assertNotNull(accessToken); assertEquals(accessToken.getToken(), ACCESS_TOKEN_STRING); assertEquals(accessToken.getScopes().size(), 2); diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 721fc885..9444905f 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -25,7 +25,7 @@ def build = [ buildToolsVersion: '26.0.2', compileSdkVersion: 26, ci: 'true' == System.getenv('CI'), - minSdkVersion: 14, + minSdkVersion: 15, targetSdkVersion: 26, repositories: [ @@ -47,6 +47,7 @@ def misc = [ def support = [ annotations: "com.android.support:support-annotations:${versions.support}", appCompat: "com.android.support:appcompat-v7:${versions.support}", + chrometabs: "com.android.support:customtabs:${versions.support}", ] def test = [ diff --git a/samples/login-sample/src/main/AndroidManifest.xml b/samples/login-sample/src/main/AndroidManifest.xml index 8949fe57..ff38adaa 100644 --- a/samples/login-sample/src/main/AndroidManifest.xml +++ b/samples/login-sample/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ --> + + + + + + diff --git a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java index 1123de90..6a42fc0a 100644 --- a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java +++ b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java @@ -138,6 +138,12 @@ protected void onResume() { } } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + loginManager.handleAuthorizationResult(intent); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(LOG_TAG, String.format("onActivityResult requestCode:[%s] resultCode [%s]", diff --git a/samples/request-button-sample/src/main/AndroidManifest.xml b/samples/request-button-sample/src/main/AndroidManifest.xml index 3bb1dfa4..6ea3b5bf 100644 --- a/samples/request-button-sample/src/main/AndroidManifest.xml +++ b/samples/request-button-sample/src/main/AndroidManifest.xml @@ -38,6 +38,13 @@ + + + + + + From 7b86262cbe9041209a2f4b48f170325c5f7c8d53 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Tue, 9 Jan 2018 16:50:21 -0800 Subject: [PATCH 02/19] Fixing tests and sample apps for partial customtab impl --- .../sdk/android/core/auth/LoginManager.java | 59 +++++++++++++------ .../android/core/auth/LoginButtonTest.java | 3 +- .../android/core/auth/LoginManagerTest.java | 4 +- .../core/auth/OAuthWebViewClientTest.java | 17 ------ .../android/rides/RideRequestActivity.java | 3 +- .../android/samples/LoginSampleActivity.java | 1 + 6 files changed, 48 insertions(+), 39 deletions(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index d8cac4c4..9c45809c 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -180,7 +180,15 @@ public void login(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForImplicitGrant(@NonNull Activity activity) { - loginWithCustomtab(activity, ResponseType.TOKEN); + final String url = AuthUtils.buildUrl(sessionConfiguration.getRedirectUri(), + ResponseType.TOKEN, sessionConfiguration); + + if (AuthUtils.isRedirectUriRegistered(activity, + Uri.parse(sessionConfiguration.getRedirectUri()))) { + loginWithCustomtab(activity, Uri.parse(url)); + } else { + loginWithWebView(activity, ResponseType.TOKEN); + } } /** @@ -189,7 +197,30 @@ public void loginForImplicitGrant(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForAuthorizationCode(@NonNull Activity activity) { - loginWithCustomtab(activity, ResponseType.CODE); + final String url = AuthUtils.buildUrl(sessionConfiguration.getRedirectUri(), + ResponseType.CODE, sessionConfiguration); + + if (AuthUtils.isRedirectUriRegistered(activity, + Uri.parse(sessionConfiguration.getRedirectUri()))) { + loginWithCustomtab(activity, Uri.parse(url)); + } else { + loginWithWebView(activity, ResponseType.CODE); + } + } + + + /** + * Deprecated to use with Ride Request Widget while transitions are being made to registered + * URI redirects + * @param activity + * @param responseType + * @deprecated + */ + @Deprecated + public void loginWithWebView(@NonNull final Activity activity, @NonNull ResponseType + responseType) { + Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, responseType); + activity.startActivityForResult(intent, requestCode); } /** @@ -311,23 +342,6 @@ public void onActivityResult( } } - private void loginWithCustomtab(@NonNull final Activity activity, @NonNull ResponseType responseType) { - - final String url = AuthUtils.buildUrl(sessionConfiguration.getRedirectUri(), responseType, - sessionConfiguration); - - if (AuthUtils.isRedirectUriRegistered(activity, - Uri.parse(sessionConfiguration.getRedirectUri()))) { - final CustomTabsIntent intent = new CustomTabsIntent.Builder().build(); - - CustomTabsHelper.openCustomTab(activity, intent, Uri.parse(url), - new CustomTabsHelper.BrowserFallback()); - } else { - Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, responseType); - activity.startActivityForResult(intent, requestCode); - } - } - /** * Handle the Uber authorization result. * This will parse the Intent to pull the access token or error out of the Data URI, and call @@ -363,6 +377,13 @@ public void handleAuthorizationResult(@NonNull Intent data) { } } + + private void loginWithCustomtab(@NonNull final Activity activity, @NonNull Uri uri) { + final CustomTabsIntent intent = new CustomTabsIntent.Builder().build(); + CustomTabsHelper.openCustomTab(activity, intent, uri, new CustomTabsHelper + .BrowserFallback()); + } + private void handleResultCancelled( @NonNull Activity activity, @Nullable Intent data) {// An error occurred during login diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java index 08fbd45f..532e61f3 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java @@ -131,7 +131,8 @@ public void testButtonClickWithoutLoginManager_shouldCreateNew() { .build(); loginButton = new LoginButton(activity, attributeSet); - loginButton.setSessionConfiguration(new SessionConfiguration.Builder().setClientId("clientId").build()); + loginButton.setSessionConfiguration(new SessionConfiguration.Builder().setRedirectUri + ("app://redirecturi").setClientId("clientId").build()); loginButton.setCallback(loginCallback); loginButton.setScopes(SCOPES); loginButton.setAccessTokenStorage(accessTokenStorage); diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java index 5d0e53f4..8a1f61f1 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java @@ -100,6 +100,7 @@ public class LoginManagerTest extends RobolectricTestBase { String.format("https://m.uber.com/sign-up?client_id=Client1234&user-agent=core-android-v%s-login_manager", BuildConfig.VERSION_NAME); private static final String AUTHORIZATION_CODE = "Auth123Code"; + private static final String REDIRECT_URI = "app://redirecturi"; @Mock Activity activity; @@ -119,7 +120,8 @@ public class LoginManagerTest extends RobolectricTestBase { @Before public void setup() { - sessionConfiguration = new SessionConfiguration.Builder().setClientId(CLIENT_ID).setScopes(MIXED_SCOPES).build(); + sessionConfiguration = new SessionConfiguration.Builder().setClientId(CLIENT_ID) + .setScopes(MIXED_SCOPES).setRedirectUri(REDIRECT_URI).build(); loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration); when(activity.getPackageManager()).thenReturn(packageManager); diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/OAuthWebViewClientTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/OAuthWebViewClientTest.java index 47f7b107..a04736c4 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/OAuthWebViewClientTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/OAuthWebViewClientTest.java @@ -67,23 +67,6 @@ public void setUp() { client = testLoginActivity.new AccessTokenClient(REDIRECT_URI); } - @Test - public void onBuildUrl_withDefaultRegion_shouldHaveDefaultUberDomain() { - String clientId = "clientId1234"; - String redirectUri = "localHost1234"; - - SessionConfiguration loginConfiguration = new SessionConfiguration.Builder() - .setRedirectUri(redirectUri) - .setScopes(Arrays.asList(Scope.HISTORY)) - .setClientId(clientId) - .build(); - - String url = testLoginActivity.buildUrl(redirectUri, ResponseType.TOKEN, loginConfiguration); - assertEquals("https://login.uber.com/oauth/v2/authorize?client_id=" + clientId + - "&redirect_uri=" + redirectUri + "&response_type=token&scope=history&" + - "show_fb=false&signup_params=eyJyZWRpcmVjdF90b19sb2dpbiI6dHJ1ZX0%3D%0A", url); - } - @Test public void onLoadLoginView_withNoRedirectUrl_shouldReturnError() { SessionConfiguration config = new SessionConfiguration.Builder().setClientId("clientId").build(); diff --git a/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java b/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java index dacfc289..d6a01cb8 100644 --- a/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java +++ b/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java @@ -41,6 +41,7 @@ import com.uber.sdk.android.core.auth.AuthenticationError; import com.uber.sdk.android.core.auth.LoginCallback; import com.uber.sdk.android.core.auth.LoginManager; +import com.uber.sdk.android.core.auth.ResponseType; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.AccessTokenStorage; import com.uber.sdk.core.auth.Scope; @@ -253,7 +254,7 @@ private void load() { } private void login() { - loginManager.login(this); + loginManager.loginWithWebView(this, ResponseType.TOKEN); } /** diff --git a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java index 6a42fc0a..403f20c8 100644 --- a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java +++ b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java @@ -154,6 +154,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { blackButton.onActivityResult(requestCode, resultCode, data); + //A temporary measure to account for older Uber app SSO implementations in the wild loginManager.onActivityResult(this, requestCode, resultCode, data); } From 60db706acc97a607d7b99bc5f823604deee9a8de Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Tue, 9 Jan 2018 16:59:45 -0800 Subject: [PATCH 03/19] Fixing browsable issue for custom tabs --- README.md | 1 + .../java/com/uber/sdk/android/core/auth/LoginManager.java | 5 ++++- samples/login-sample/src/main/AndroidManifest.xml | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d26176a2..6d984bd5 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ short term until we indicate otherwise. + diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index 9c45809c..6e991f6a 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -448,7 +448,10 @@ private void validateRedirectUriRegistration(@NonNull Activity activity) { if (!AuthUtils.isRedirectUriRegistered(activity, Uri.parse(sessionConfiguration .getRedirectUri()))) { - String error = "Must now register redirect_uri in AndroidManifest.xml. See README."; + String error = "Must now register redirect_uri " + sessionConfiguration + .getRedirectUri() + " in an intent filter in the AndroidManifest.xml of the " + + "application. See README in the rides-android-sdk for more information."; + if (Utility.isDebugable(activity)) { throw new IllegalStateException(error); } else { diff --git a/samples/login-sample/src/main/AndroidManifest.xml b/samples/login-sample/src/main/AndroidManifest.xml index ff38adaa..c61f4179 100644 --- a/samples/login-sample/src/main/AndroidManifest.xml +++ b/samples/login-sample/src/main/AndroidManifest.xml @@ -42,6 +42,7 @@ + From e071465cf69f5a027ab10877f2beafed4d4fc6f7 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Tue, 9 Jan 2018 18:53:09 -0800 Subject: [PATCH 04/19] Unifying callback for SSO and redirect URIs --- .../android/core/auth/CustomTabsHelper.java | 2 +- .../sdk/android/core/auth/LoginButton.java | 5 + .../sdk/android/core/auth/LoginManager.java | 128 ++++++++---------- .../android/core/auth/LoginManagerTest.java | 28 +--- .../android/rides/RideRequestActivity.java | 2 +- .../rides/RideRequestActivityTest.java | 4 +- .../android/samples/LoginSampleActivity.java | 15 +- 7 files changed, 67 insertions(+), 117 deletions(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java index 631a192e..3c6ed7c1 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java @@ -68,7 +68,7 @@ public static void openCustomTab(Activity activity, } } else { customTabsIntent.intent.setPackage(packageName); - customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); customTabsIntent.launchUrl(activity, uri); } } diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginButton.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginButton.java index ee38ad6c..2e5c00f1 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginButton.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginButton.java @@ -227,6 +227,11 @@ public int getRequestCode() { * @param requestCode request code originally supplied to {@link Activity#startActivityForResult(Intent, int)}. * @param resultCode result code from returning {@link Activity}. * @param data data from returning {@link Activity}. + * + * @deprecated use {@link LoginManager#handleAuthorizationResult(Activity, Intent)} going + * forward. Will be removed in future version once all + * startActivityForResult/onActivityResult code for implicit grant and SSO is switched to + * using redirect URIs. */ public void onActivityResult( int requestCode, diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index 6e991f6a..e8096f7d 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -107,7 +107,7 @@ public class LoginManager { /** * @param accessTokenStorage to store access token. - * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} + * @param loginCallback callback to be called when {@link LoginManager#handleAuthorizationResult(Activity, Intent)} * is called. */ public LoginManager( @@ -118,7 +118,7 @@ public LoginManager( /** * @param accessTokenStorage to store access token. - * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} is called. + * @param loginCallback callback to be called when {@link LoginManager#handleAuthorizationResult(Activity, Intent)} is called. * @param configuration to provide authentication information */ public LoginManager( @@ -130,7 +130,7 @@ public LoginManager( /** * @param accessTokenStorage to store access token. - * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} is called. + * @param loginCallback callback to be called when {@link LoginManager#handleAuthorizationResult(Activity, Intent)} is called. * @param configuration to provide authentication information * @param requestCode custom code to use for Activity communication */ @@ -325,31 +325,33 @@ private void redirectToInstallApp(@NonNull Activity activity) { * @param requestCode request code originally supplied to {@link Activity#startActivityForResult(Intent, int)}. * @param resultCode result code from returning {@link Activity}. * @param data data from returning {@link Activity}. + * + * @deprecated use {@link LoginManager#handleAuthorizationResult(Activity, Intent)} going + * forward. Will be removed in future version once all + * startActivityForResult/onActivityResult code for implicit grant and SSO is switched to + * using redirect URIs. */ + @Deprecated public void onActivityResult( @NonNull Activity activity, int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode != this.requestCode) { - return; - } - - if (resultCode == Activity.RESULT_OK) { - handleResultOk(data); - } else if (resultCode == Activity.RESULT_CANCELED) { - handleResultCancelled(activity, data); - } + handleAuthorizationResult(activity, data); } /** * Handle the Uber authorization result. * This will parse the Intent to pull the access token or error out of the Data URI, and call - * the set callback. + * the set callback. This will no-op when no Uber Tokens are present. * * @param data */ - public void handleAuthorizationResult(@NonNull Intent data) { + public void handleAuthorizationResult(@NonNull Activity activity, @Nullable Intent data) { + if(data == null) { + return; + } + if (data.getData() != null && data.getData().toString().startsWith(sessionConfiguration.getRedirectUri())) { @@ -370,10 +372,48 @@ public void handleAuthorizationResult(@NonNull Intent data) { try { AccessToken token = AuthUtils.parseTokenUri(fragmentUri); + accessTokenStorage.setAccessToken(token); callback.onLoginSuccess(token); } catch (LoginAuthenticationException e) { callback.onLoginError(e.getAuthenticationError()); } + } else if(data.getStringExtra(EXTRA_ERROR) != null) { + final String error = data.getStringExtra(EXTRA_ERROR); + final AuthenticationError authenticationError + = (error != null) ? AuthenticationError.fromString(error) : AuthenticationError.UNKNOWN; + + if (authenticationError.equals(AuthenticationError.CANCELLED)) { + // User canceled login + callback.onLoginCancel(); + return; + } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) && + !AuthUtils.isPrivilegeScopeRequired(sessionConfiguration.getScopes())) { + loginForImplicitGrant(activity); + return; + } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) + && redirectForAuthorizationCode) { + loginForAuthorizationCode(activity); + } else if (AuthenticationError.INVALID_APP_SIGNATURE.equals(authenticationError)) { + AppProtocol appProtocol = new AppProtocol(); + String appSignature = appProtocol.getAppSignature(activity); + if (appSignature == null) { + Log.e(UberSdk.UBER_SDK_LOG_TAG, "There was an error obtaining your Application Signature. Please check " + + "your Application Signature and add it to the developer dashboard at https://developer.uber" + + ".com/dashboard"); + } else { + Log.e(UberSdk.UBER_SDK_LOG_TAG, "Your Application Signature, " + appSignature + + ", does not match one of the registered Application Signatures on the developer dashboard. " + + "Check your settings at https://developer.uber.com/dashboard"); + } + } + callback.onLoginError(authenticationError); + } else if(data.getStringExtra(EXTRA_CODE_RECEIVED) != null) { + final String authorizationCode = data.getStringExtra(EXTRA_CODE_RECEIVED); + callback.onAuthorizationCodeReceived(authorizationCode); + } else if (data.getStringExtra(LoginManager.EXTRA_ACCESS_TOKEN) != null) { + AccessToken accessToken = AuthUtils.createAccessToken(data); + accessTokenStorage.setAccessToken(accessToken); + callback.onLoginSuccess(accessToken); } } @@ -384,66 +424,6 @@ private void loginWithCustomtab(@NonNull final Activity activity, @NonNull Uri u .BrowserFallback()); } - private void handleResultCancelled( - @NonNull Activity activity, - @Nullable Intent data) {// An error occurred during login - - if (data == null) { - // User canceled login - callback.onLoginCancel(); - return; - } - - final String error = data.getStringExtra(EXTRA_ERROR); - final AuthenticationError authenticationError - = (error != null) ? AuthenticationError.fromString(error) : AuthenticationError.UNKNOWN; - - if (authenticationError.equals(AuthenticationError.CANCELLED)) { - // User canceled login - callback.onLoginCancel(); - return; - } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) && - !AuthUtils.isPrivilegeScopeRequired(sessionConfiguration.getScopes())) { - loginForImplicitGrant(activity); - return; - } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) - && redirectForAuthorizationCode) { - loginForAuthorizationCode(activity); - } else if (AuthenticationError.INVALID_APP_SIGNATURE.equals(authenticationError)) { - AppProtocol appProtocol = new AppProtocol(); - String appSignature = appProtocol.getAppSignature(activity); - if (appSignature == null) { - Log.e(UberSdk.UBER_SDK_LOG_TAG, "There was an error obtaining your Application Signature. Please check " - + "your Application Signature and add it to the developer dashboard at https://developer.uber" - + ".com/dashboard"); - } else { - Log.e(UberSdk.UBER_SDK_LOG_TAG, "Your Application Signature, " + appSignature - + ", does not match one of the registered Application Signatures on the developer dashboard. " - + "Check your settings at https://developer.uber.com/dashboard"); - } - } - callback.onLoginError(authenticationError); - } - - private void handleResultOk(@Nullable Intent data) { - if (data == null) { - // Unknown error, should never occur - callback.onLoginError(AuthenticationError.UNKNOWN); - return; - } - - final String authorizationCode = data.getStringExtra(EXTRA_CODE_RECEIVED); - if (authorizationCode != null) { - callback.onAuthorizationCodeReceived(authorizationCode); - } else { - AccessToken accessToken = AuthUtils.createAccessToken(data); - accessTokenStorage.setAccessToken(accessToken); - - callback.onLoginSuccess(accessToken); - - } - } - private void validateRedirectUriRegistration(@NonNull Activity activity) { if (!AuthUtils.isRedirectUriRegistered(activity, Uri.parse(sessionConfiguration .getRedirectUri()))) { diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java index 8a1f61f1..d4664cd8 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java @@ -234,12 +234,6 @@ public void onActivityResult_whenResultOkAndHasCode_shouldCallbackSuccess() { assertThat(capturedCode.getValue()).isEqualTo(AUTHORIZATION_CODE); } - @Test - public void onActivityResult_whenResultCanceledAndNoData_shouldCallbackCancel() { - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); - verify(callback).onLoginCancel(); - } - @Test public void onActivityResult_whenResultCanceledAndHasData_shouldCallbackError() { Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.INVALID_RESPONSE @@ -249,31 +243,11 @@ public void onActivityResult_whenResultCanceledAndHasData_shouldCallbackError() verify(callback).onLoginError(AuthenticationError.INVALID_RESPONSE); } - @Test - public void onActivityResult_whenResultCanceledAndNoData_shouldCancel() { - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); - verify(callback).onLoginCancel(); - } - - @Test - public void onActivityResult_whenResultOkAndNoData_shouldCallbackErrorUnknown() { - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, null); - verify(callback).onLoginError(AuthenticationError.UNKNOWN); - } - - @Test - public void onActivityResult_whenRequestCodeDoesNotMatch_nothingShouldHappen() { - Intent intent = mock(Intent.class); - loginManager.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); - verifyZeroInteractions(intent); - verifyZeroInteractions(callback); - } - @Test public void onActivityResult_whenResultCanceledAndDataButNoCallback_nothingShouldHappen() { Intent intent = mock(Intent.class); loginManager.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); - verifyZeroInteractions(intent); + verifyZeroInteractions(callback); } @Test diff --git a/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java b/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java index d6a01cb8..c49c282e 100644 --- a/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java +++ b/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java @@ -156,7 +156,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == LOGIN_REQUEST_CODE) { - loginManager.onActivityResult(this, requestCode, resultCode, data); + loginManager.handleAuthorizationResult(this, data); } } diff --git a/rides-android/src/test/java/com/uber/sdk/android/rides/RideRequestActivityTest.java b/rides-android/src/test/java/com/uber/sdk/android/rides/RideRequestActivityTest.java index 19d7a58d..0a808452 100644 --- a/rides-android/src/test/java/com/uber/sdk/android/rides/RideRequestActivityTest.java +++ b/rides-android/src/test/java/com/uber/sdk/android/rides/RideRequestActivityTest.java @@ -30,6 +30,7 @@ import com.uber.sdk.android.core.auth.AccessTokenManager; import com.uber.sdk.android.core.auth.AuthenticationError; import com.uber.sdk.android.core.auth.LoginManager; +import com.uber.sdk.android.core.auth.ResponseType; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.AccessTokenStorage; import com.uber.sdk.core.auth.Scope; @@ -47,6 +48,7 @@ import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.eq; import static org.mockito.Matchers.refEq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -199,7 +201,7 @@ public void onLoad_whenRideRequestViewAuthorizationErrorOccurs_shouldAttemptLogi assertNull(shadowActivity.getResultIntent()); verify(activity.accessTokenStorage, times(1)).removeAccessToken(); - verify(activity.loginManager).login(refEq(activity)); + verify(activity.loginManager).loginWithWebView(refEq(activity), eq(ResponseType.TOKEN)); } @Test diff --git a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java index 403f20c8..1508dbec 100644 --- a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java +++ b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java @@ -90,7 +90,6 @@ public class LoginSampleActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sample); - configuration = new SessionConfiguration.Builder() .setClientId(CLIENT_ID) .setRedirectUri(REDIRECT_URI) @@ -120,6 +119,7 @@ protected void onCreate(Bundle savedInstanceState) { new SampleLoginCallback(), configuration, CUSTOM_BUTTON_REQUEST_CODE); + loginManager.handleAuthorizationResult(this, getIntent()); customButton = (Button) findViewById(R.id.custom_uber_button); customButton.setOnClickListener(new View.OnClickListener() { @@ -138,24 +138,13 @@ protected void onResume() { } } - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - loginManager.handleAuthorizationResult(intent); - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(LOG_TAG, String.format("onActivityResult requestCode:[%s] resultCode [%s]", requestCode, resultCode)); - //Allow each a chance to catch it. - whiteButton.onActivityResult(requestCode, resultCode, data); - - blackButton.onActivityResult(requestCode, resultCode, data); - //A temporary measure to account for older Uber app SSO implementations in the wild - loginManager.onActivityResult(this, requestCode, resultCode, data); + loginManager.handleAuthorizationResult(this, data); } private class SampleLoginCallback implements LoginCallback { From e2be3aca793405d2e774b959b47ad22081e0f419 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Thu, 11 Jan 2018 14:05:10 -0800 Subject: [PATCH 05/19] Redirect URI param added for SSO --- .../sdk/android/core/auth/LoginManager.java | 1 + .../sdk/android/core/auth/SsoDeeplink.java | 18 ++++++++++++++++-- .../sdk/android/core/utils/AppProtocol.java | 3 ++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index e8096f7d..e8a43bdc 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -161,6 +161,7 @@ public void login(@NonNull Activity activity) { .scopes(sessionConfiguration.getScopes()) .customScopes(sessionConfiguration.getCustomScopes()) .activityRequestCode(requestCode) + .redirectUri(sessionConfiguration.getRedirectUri()) .build(); if (ssoDeeplink.isSupported()) { diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/SsoDeeplink.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/SsoDeeplink.java index a1cc8975..7d8280fd 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/SsoDeeplink.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/SsoDeeplink.java @@ -61,12 +61,14 @@ public class SsoDeeplink implements Deeplink { private static final String URI_QUERY_PLATFORM = "sdk"; private static final String URI_QUERY_SDK_VERSION = "sdk_version"; private static final String URI_HOST = "connect"; + private static final String URI_QUERY_REDIRECT_URI = "redirect_uri"; private final Activity activity; private final String clientId; private final Collection requestedScopes; private final Collection requestedCustomScopes; private final int requestCode; + private final String redirectUri; AppProtocol appProtocol; @@ -75,12 +77,14 @@ public class SsoDeeplink implements Deeplink { @NonNull String clientId, @NonNull Collection requestedScopes, @NonNull Collection requestedCustomScopes, - int requestCode) { + int requestCode, + @NonNull String redirectUri) { this.activity = activity; this.clientId = clientId; this.requestCode = requestCode; this.requestedScopes = requestedScopes; this.requestedCustomScopes = requestedCustomScopes; + this.redirectUri = redirectUri; appProtocol = new AppProtocol(); } @@ -121,6 +125,7 @@ private Uri createSsoUri() { .appendQueryParameter(URI_QUERY_SCOPE, scopes) .appendQueryParameter(URI_QUERY_PLATFORM, AppProtocol.PLATFORM) .appendQueryParameter(URI_QUERY_SDK_VERSION, BuildConfig.VERSION_NAME) + .appendQueryParameter(URI_QUERY_REDIRECT_URI, redirectUri) .build(); } @@ -153,6 +158,7 @@ public static class Builder { private Collection requestedScopes; private Collection requestedCustomScopes; private int requestCode = DEFAULT_REQUEST_CODE; + private String redirectUri; public Builder(@NonNull Activity activity) { this.activity = activity; @@ -183,8 +189,15 @@ public Builder activityRequestCode(int requestCode) { return this; } + + public Builder redirectUri(@NonNull String redirectUri) { + this.redirectUri = redirectUri; + return this; + } + public SsoDeeplink build() { checkNotNull(clientId, "Client Id must be set"); + checkNotNull(redirectUri, "redirectUri must be set"); checkNotEmpty(requestedScopes, "Scopes must be set."); @@ -195,7 +208,8 @@ public SsoDeeplink build() { if (requestCode == DEFAULT_REQUEST_CODE) { Log.i(UBER_SDK_LOG_TAG, "Request code is not set, using default request code"); } - return new SsoDeeplink(activity, clientId, requestedScopes, requestedCustomScopes, requestCode); + return new SsoDeeplink(activity, clientId, requestedScopes, requestedCustomScopes, + requestCode, redirectUri); } } } diff --git a/core-android/src/main/java/com/uber/sdk/android/core/utils/AppProtocol.java b/core-android/src/main/java/com/uber/sdk/android/core/utils/AppProtocol.java index 61f8c1e4..8e631a2c 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/utils/AppProtocol.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/utils/AppProtocol.java @@ -18,7 +18,8 @@ public class AppProtocol { public static final String[] UBER_PACKAGE_NAMES = - {"com.ubercab", "com.ubercab.presidio.app", "com.ubercab.presidio.exo"}; + {"com.ubercab", "com.ubercab.presidio.app", "com.ubercab.presidio.exo", + "com.ubercab.presidio.development"}; public static final String DEEPLINK_SCHEME = "uber"; public static final String PLATFORM = "android"; From a1d78fb971bbd03e0046bd8e460efaa18a02f032 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Thu, 11 Jan 2018 16:19:13 -0800 Subject: [PATCH 06/19] Creating LoginMangaer as proxy --- core-android/src/main/AndroidManifest.xml | 15 ++- .../android/core/auth/CustomTabsHelper.java | 41 +++++- .../sdk/android/core/auth/LoginActivity.java | 125 ++++++++++++++---- .../auth/LoginRedirectReceiverActivity.java | 20 +++ rides-android/src/main/AndroidManifest.xml | 3 - .../login-sample/src/main/AndroidManifest.xml | 8 -- .../android/samples/LoginSampleActivity.java | 2 + 7 files changed, 168 insertions(+), 46 deletions(-) create mode 100644 core-android/src/main/java/com/uber/sdk/android/core/auth/LoginRedirectReceiverActivity.java diff --git a/core-android/src/main/AndroidManifest.xml b/core-android/src/main/AndroidManifest.xml index 0784cba1..e4cb8e11 100644 --- a/core-android/src/main/AndroidManifest.xml +++ b/core-android/src/main/AndroidManifest.xml @@ -29,6 +29,19 @@ + android:screenOrientation="portrait" + android:launchMode="singleTask"> + + + + + + + + + + diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java index 3c6ed7c1..85d11254 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java @@ -15,6 +15,7 @@ package com.uber.sdk.android.core.auth; import android.app.Activity; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -23,7 +24,9 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.customtabs.CustomTabsClient; import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; import android.text.TextUtils; import android.util.Log; @@ -56,21 +59,45 @@ private CustomTabsHelper() {} * @param uri the Uri to be opened. * @param fallback a CustomTabFallback to be used if Custom Tabs is not available. */ - public static void openCustomTab(Activity activity, - CustomTabsIntent customTabsIntent, - Uri uri, + public static void openCustomTab( + final Activity activity, + final CustomTabsIntent customTabsIntent, + final Uri uri, CustomTabFallback fallback) { - String packageName = getPackageNameToUse(activity); + final String packageName = getPackageNameToUse(activity); if (packageName == null) { if (fallback != null) { fallback.openUri(activity, uri); } } else { - customTabsIntent.intent.setPackage(packageName); - customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - customTabsIntent.launchUrl(activity, uri); + final CustomTabsServiceConnection connection = new CustomTabsServiceConnection() { + @Override + public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) { + client.warmup(0L); // This prevents backgrounding after redirection + + customTabsIntent.intent.setPackage(packageName); + customTabsIntent.intent.setData(uri); + customTabsIntent.launchUrl(activity, uri); + } + @Override + public void onServiceDisconnected(ComponentName name) {} + }; + CustomTabsClient.bindCustomTabsService(activity, packageName, connection); } +// customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | +// Intent.FLAG_ACTIVITY_CLEAR_TASK); +// customTabsIntent.intent.setPackage(packageName); +// customTabsIntent.intent.setData(uri); +// activity.startActivityForResult(customTabsIntent.intent, 20); +// customTabsIntent.launchUrl(activity, uri); + +// } + } + + + static void launchTab(final Context context, final Uri uri){ + } /** diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java index 2d86caa0..021182f3 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java @@ -29,6 +29,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; +import android.support.customtabs.CustomTabsIntent; import android.text.TextUtils; import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; @@ -46,10 +47,14 @@ public class LoginActivity extends Activity { static final String EXTRA_RESPONSE_TYPE = "RESPONSE_TYPE"; static final String EXTRA_SESSION_CONFIGURATION = "SESSION_CONFIGURATION"; + static final String EXTRA_FORCE_WEBVIEW = "FORCE_WEBVIEW"; + + static final String ERROR = "error"; private WebView webView; private ResponseType responseType; private SessionConfiguration sessionConfiguration; + private LoginManager loginManager; /** * Create an {@link Intent} to pass to this activity @@ -65,20 +70,69 @@ public static Intent newIntent( @NonNull SessionConfiguration sessionConfiguration, @NonNull ResponseType responseType) { + return newIntent(context, sessionConfiguration, responseType, false); + } + + /** + * Create an {@link Intent} to pass to this activity + * + * @param context the {@link Context} for the intent + * @param sessionConfiguration to be used for gather clientId + * @param responseType that is expected + * @param forceWebview Forced to use old webview instead of chrometabs + * @return an intent that can be passed to this activity + */ + @NonNull + public static Intent newIntent( + @NonNull Context context, + @NonNull SessionConfiguration sessionConfiguration, + @NonNull ResponseType responseType, + boolean forceWebview) { + final Intent data = new Intent(context, LoginActivity.class) .putExtra(EXTRA_SESSION_CONFIGURATION, sessionConfiguration) - .putExtra(EXTRA_RESPONSE_TYPE, responseType); + .putExtra(EXTRA_RESPONSE_TYPE, responseType) + .putExtra(EXTRA_FORCE_WEBVIEW, forceWebview); return data; } + /** + * Used to handle Redirect URI response from customtab or browser + * + * @param context + * @param responseUri + * @return + */ + public static Intent newResponseIntent(Context context, Uri responseUri) { + Intent intent = new Intent(context, LoginActivity.class); + intent.setData(responseUri); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + return intent; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.ub__login_activity); + init(); + } - webView = (WebView) findViewById(R.id.ub__login_webview); + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + init(); + } + + protected void init() { + if(getIntent().getData() != null) { + handleResponse(getIntent().getData()); + } else { + loadUrl(); + } + } + protected void loadUrl() { sessionConfiguration = (SessionConfiguration) getIntent().getSerializableExtra(EXTRA_SESSION_CONFIGURATION); if (sessionConfiguration == null) { onError(AuthenticationError.UNAVAILABLE); @@ -102,11 +156,50 @@ protected void onCreate(Bundle savedInstanceState) { return; } + String url = AuthUtils.buildUrl(redirectUri, responseType, sessionConfiguration); + + if (getIntent().getBooleanExtra(EXTRA_FORCE_WEBVIEW, false)) { + loadWebview(url, redirectUri); + } else { + loadChrometab(url); + } + } + + protected boolean handleResponse(@NonNull Uri uri) { + final String fragment = uri.getFragment(); + + if (fragment == null) { + onError(AuthenticationError.INVALID_RESPONSE); + return true; + } + + final Uri fragmentUri = new Uri.Builder().encodedQuery(fragment).build(); + + // In case fragment contains error, we want to handle that too. + final String error = fragmentUri.getQueryParameter(ERROR); + if (!TextUtils.isEmpty(error)) { + onError(AuthenticationError.fromString(error)); + return true; + } + + onTokenReceived(fragmentUri); + return true; + } + + protected void loadWebview(String url, String redirectUri) { + setContentView(R.layout.ub__login_activity); + webView = (WebView) findViewById(R.id.ub__login_webview); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setAppCacheEnabled(true); webView.getSettings().setDomStorageEnabled(true); webView.setWebViewClient(createOAuthClient(redirectUri)); - webView.loadUrl(AuthUtils.buildUrl(redirectUri, responseType, sessionConfiguration)); + webView.loadUrl(url); + } + + protected void loadChrometab(String url) { + final CustomTabsIntent intent = new CustomTabsIntent.Builder().build(); + CustomTabsHelper.openCustomTab(this, intent, Uri.parse(url), new CustomTabsHelper + .BrowserFallback()); } protected OAuthWebViewClient createOAuthClient(String redirectUri) { @@ -154,8 +247,6 @@ void onCodeReceived(Uri uri) { @VisibleForTesting abstract class OAuthWebViewClient extends WebViewClient { - protected static final String ERROR = "error"; - @NonNull protected final String redirectUri; @@ -213,27 +304,7 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { } if (url.startsWith(redirectUri)) { - - //OAuth 2 spec requires the access token in URL Fragment instead of query parameters. - //Swap Fragment with Query to facilitate parsing. - final String fragment = uri.getFragment(); - - if (fragment == null) { - onError(AuthenticationError.INVALID_RESPONSE); - return true; - } - - final Uri fragmentUri = new Uri.Builder().encodedQuery(fragment).build(); - - // In case fragment contains error, we want to handle that too. - final String error = fragmentUri.getQueryParameter(ERROR); - if (!TextUtils.isEmpty(error)) { - onError(AuthenticationError.fromString(error)); - return true; - } - - onTokenReceived(fragmentUri); - return true; + return handleResponse(uri); } return super.shouldOverrideUrlLoading(view, url); diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginRedirectReceiverActivity.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginRedirectReceiverActivity.java new file mode 100644 index 00000000..90060876 --- /dev/null +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginRedirectReceiverActivity.java @@ -0,0 +1,20 @@ +package com.uber.sdk.android.core.auth; + +import android.app.Activity; +import android.os.Bundle; + +public class LoginRedirectReceiverActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceBundle) { + super.onCreate(savedInstanceBundle); + + // while this does not appear to be achieving much, handling the redirect in this way + // ensures that we can remove the browser tab from the back stack. See the documentation + // on AuthorizationManagementActivity for more details. + startActivity(LoginActivity.newResponseIntent( + this, getIntent().getData())); + finish(); + } + +} diff --git a/rides-android/src/main/AndroidManifest.xml b/rides-android/src/main/AndroidManifest.xml index aafec2fb..33c1f5fa 100644 --- a/rides-android/src/main/AndroidManifest.xml +++ b/rides-android/src/main/AndroidManifest.xml @@ -30,8 +30,5 @@ - diff --git a/samples/login-sample/src/main/AndroidManifest.xml b/samples/login-sample/src/main/AndroidManifest.xml index c61f4179..3bb26e75 100644 --- a/samples/login-sample/src/main/AndroidManifest.xml +++ b/samples/login-sample/src/main/AndroidManifest.xml @@ -38,14 +38,6 @@ - - - - - - - diff --git a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java index 1508dbec..927b67a9 100644 --- a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java +++ b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java @@ -41,6 +41,7 @@ import com.uber.sdk.android.core.auth.LoginButton; import com.uber.sdk.android.core.auth.LoginCallback; import com.uber.sdk.android.core.auth.LoginManager; +import com.uber.sdk.android.core.auth.ResponseType; import com.uber.sdk.android.rides.samples.BuildConfig; import com.uber.sdk.android.rides.samples.R; import com.uber.sdk.core.auth.AccessToken; @@ -125,6 +126,7 @@ protected void onCreate(Bundle savedInstanceState) { customButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + loginManager.login(LoginSampleActivity.this); } }); From e7b99e5c6a33e25793d63fd134cfbefcacfdbfea Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Thu, 11 Jan 2018 17:26:56 -0800 Subject: [PATCH 07/19] Removing uneeded changes --- .../uber/sdk/android/core/auth/AuthUtils.java | 20 +- .../sdk/android/core/auth/LoginActivity.java | 24 ++- .../sdk/android/core/auth/LoginButton.java | 5 - .../sdk/android/core/auth/LoginManager.java | 198 +++++++----------- .../sdk/android/core/auth/SsoDeeplink.java | 22 +- .../sdk/android/core/auth/AuthUtilsTest.java | 19 ++ .../android/core/auth/LoginButtonTest.java | 3 +- .../android/core/auth/LoginManagerTest.java | 32 ++- .../android/rides/RideRequestActivity.java | 11 +- .../rides/RideRequestActivityTest.java | 4 +- .../android/samples/LoginSampleActivity.java | 50 ++--- 11 files changed, 183 insertions(+), 205 deletions(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/AuthUtils.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/AuthUtils.java index b7e539b1..cc8a6b1e 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/AuthUtils.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/AuthUtils.java @@ -23,7 +23,9 @@ package com.uber.sdk.android.core.auth; import android.app.Activity; +import android.content.ComponentName; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.ResolveInfo; import android.net.Uri; import android.support.annotation.NonNull; @@ -129,17 +131,15 @@ static Collection stringCollectionToScopeCollection(@NonNull Collection resolvedActivityList = activity.getPackageManager() - .queryIntentActivities(activityIntent, 0); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(uri); + ComponentName info = intent.resolveActivity(activity.getPackageManager()); + + return info != null && info.getClassName().equals(LoginRedirectReceiverActivity.class + .getName()); + - boolean supported = false; - for (ResolveInfo resolveInfo : resolvedActivityList) { - if (resolveInfo.activityInfo.packageName.equals(activity.getPackageName())) { - supported = true; - } - } - return supported; } /** diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java index 021182f3..b634bc83 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java @@ -55,6 +55,7 @@ public class LoginActivity extends Activity { private ResponseType responseType; private SessionConfiguration sessionConfiguration; private LoginManager loginManager; + private boolean authStarted; /** * Create an {@link Intent} to pass to this activity @@ -117,9 +118,24 @@ protected void onCreate(Bundle savedInstanceState) { init(); } + @Override + protected void onResume() { + super.onResume(); + + if(webView == null) { + if(!authStarted) { + authStarted = true; + return; + } + + finish(); + } + } + @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); + authStarted = false; setIntent(intent); init(); } @@ -150,14 +166,10 @@ protected void loadUrl() { onError(AuthenticationError.UNAVAILABLE); } - final String redirectUri = sessionConfiguration.getRedirectUri(); - if (redirectUri == null) { - onError(AuthenticationError.INVALID_REDIRECT_URI); - return; - } + String redirectUri = sessionConfiguration.getRedirectUri() != null ? sessionConfiguration + .getRedirectUri() : getApplicationContext().getPackageName() + "uberauth"; String url = AuthUtils.buildUrl(redirectUri, responseType, sessionConfiguration); - if (getIntent().getBooleanExtra(EXTRA_FORCE_WEBVIEW, false)) { loadWebview(url, redirectUri); } else { diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginButton.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginButton.java index 2e5c00f1..ee38ad6c 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginButton.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginButton.java @@ -227,11 +227,6 @@ public int getRequestCode() { * @param requestCode request code originally supplied to {@link Activity#startActivityForResult(Intent, int)}. * @param resultCode result code from returning {@link Activity}. * @param data data from returning {@link Activity}. - * - * @deprecated use {@link LoginManager#handleAuthorizationResult(Activity, Intent)} going - * forward. Will be removed in future version once all - * startActivityForResult/onActivityResult code for implicit grant and SSO is switched to - * using redirect URIs. */ public void onActivityResult( int requestCode, diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index e8a43bdc..93110a9d 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -27,8 +27,6 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.customtabs.CustomTabsIntent; -import android.text.TextUtils; import android.util.Log; import com.uber.sdk.android.core.BuildConfig; @@ -58,11 +56,6 @@ public class LoginManager { */ static final String EXTRA_ERROR = "ERROR"; - /** - * Used to retrieve the {@link AuthenticationError} from a result URI - */ - static final String QUERY_PARAM_ERROR = "error"; - /** * Used to retrieve the Access Token from an {@link Intent}. */ @@ -107,7 +100,7 @@ public class LoginManager { /** * @param accessTokenStorage to store access token. - * @param loginCallback callback to be called when {@link LoginManager#handleAuthorizationResult(Activity, Intent)} + * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} * is called. */ public LoginManager( @@ -118,7 +111,7 @@ public LoginManager( /** * @param accessTokenStorage to store access token. - * @param loginCallback callback to be called when {@link LoginManager#handleAuthorizationResult(Activity, Intent)} is called. + * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} is called. * @param configuration to provide authentication information */ public LoginManager( @@ -130,7 +123,7 @@ public LoginManager( /** * @param accessTokenStorage to store access token. - * @param loginCallback callback to be called when {@link LoginManager#handleAuthorizationResult(Activity, Intent)} is called. + * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} is called. * @param configuration to provide authentication information * @param requestCode custom code to use for Activity communication */ @@ -154,14 +147,11 @@ public void login(@NonNull Activity activity) { checkNotEmpty(sessionConfiguration.getScopes(), "Scopes must be set in the Session " + "Configuration."); - validateRedirectUriRegistration(activity); - SsoDeeplink ssoDeeplink = new SsoDeeplink.Builder(activity) .clientId(sessionConfiguration.getClientId()) .scopes(sessionConfiguration.getScopes()) .customScopes(sessionConfiguration.getCustomScopes()) .activityRequestCode(requestCode) - .redirectUri(sessionConfiguration.getRedirectUri()) .build(); if (ssoDeeplink.isSupported()) { @@ -181,15 +171,9 @@ public void login(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForImplicitGrant(@NonNull Activity activity) { - final String url = AuthUtils.buildUrl(sessionConfiguration.getRedirectUri(), - ResponseType.TOKEN, sessionConfiguration); - - if (AuthUtils.isRedirectUriRegistered(activity, - Uri.parse(sessionConfiguration.getRedirectUri()))) { - loginWithCustomtab(activity, Uri.parse(url)); - } else { - loginWithWebView(activity, ResponseType.TOKEN); - } + validateRedirectUriRegistration(activity); + Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, ResponseType.TOKEN); + activity.startActivityForResult(intent, requestCode); } /** @@ -198,29 +182,8 @@ public void loginForImplicitGrant(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForAuthorizationCode(@NonNull Activity activity) { - final String url = AuthUtils.buildUrl(sessionConfiguration.getRedirectUri(), - ResponseType.CODE, sessionConfiguration); - - if (AuthUtils.isRedirectUriRegistered(activity, - Uri.parse(sessionConfiguration.getRedirectUri()))) { - loginWithCustomtab(activity, Uri.parse(url)); - } else { - loginWithWebView(activity, ResponseType.CODE); - } - } - - - /** - * Deprecated to use with Ride Request Widget while transitions are being made to registered - * URI redirects - * @param activity - * @param responseType - * @deprecated - */ - @Deprecated - public void loginWithWebView(@NonNull final Activity activity, @NonNull ResponseType - responseType) { - Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, responseType); + validateRedirectUriRegistration(activity); + Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, ResponseType.CODE); activity.startActivityForResult(intent, requestCode); } @@ -326,112 +289,93 @@ private void redirectToInstallApp(@NonNull Activity activity) { * @param requestCode request code originally supplied to {@link Activity#startActivityForResult(Intent, int)}. * @param resultCode result code from returning {@link Activity}. * @param data data from returning {@link Activity}. - * - * @deprecated use {@link LoginManager#handleAuthorizationResult(Activity, Intent)} going - * forward. Will be removed in future version once all - * startActivityForResult/onActivityResult code for implicit grant and SSO is switched to - * using redirect URIs. */ - @Deprecated public void onActivityResult( @NonNull Activity activity, int requestCode, int resultCode, @Nullable Intent data) { - handleAuthorizationResult(activity, data); - } - - /** - * Handle the Uber authorization result. - * This will parse the Intent to pull the access token or error out of the Data URI, and call - * the set callback. This will no-op when no Uber Tokens are present. - * - * @param data - */ - public void handleAuthorizationResult(@NonNull Activity activity, @Nullable Intent data) { - if(data == null) { + if (requestCode != this.requestCode) { return; } - if (data.getData() != null - && data.getData().toString().startsWith(sessionConfiguration.getRedirectUri())) { + if (resultCode == Activity.RESULT_OK) { + handleResultOk(data); + } else if (resultCode == Activity.RESULT_CANCELED) { + handleResultCancelled(activity, data); + } + } - final String fragment = data.getData().getFragment(); + private void handleResultCancelled( + @NonNull Activity activity, + @Nullable Intent data) {// An error occurred during login - if (fragment == null) { - callback.onLoginError(AuthenticationError.INVALID_RESPONSE); - return; - } + if (data == null) { + // User canceled login + callback.onLoginCancel(); + return; + } - final Uri fragmentUri = new Uri.Builder().encodedQuery(fragment).build(); + final String error = data.getStringExtra(EXTRA_ERROR); + final AuthenticationError authenticationError + = (error != null) ? AuthenticationError.fromString(error) : AuthenticationError.UNKNOWN; - final String error = fragmentUri.getQueryParameter(QUERY_PARAM_ERROR); - if (!TextUtils.isEmpty(error)) { - callback.onLoginError(AuthenticationError.fromString(error)); - return; + if (authenticationError.equals(AuthenticationError.CANCELLED)) { + // User canceled login + callback.onLoginCancel(); + return; + } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) && + !AuthUtils.isPrivilegeScopeRequired(sessionConfiguration.getScopes())) { + loginForImplicitGrant(activity); + return; + } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) + && redirectForAuthorizationCode) { + loginForAuthorizationCode(activity); + } else if (AuthenticationError.INVALID_APP_SIGNATURE.equals(authenticationError)) { + AppProtocol appProtocol = new AppProtocol(); + String appSignature = appProtocol.getAppSignature(activity); + if (appSignature == null) { + Log.e(UberSdk.UBER_SDK_LOG_TAG, "There was an error obtaining your Application Signature. Please check " + + "your Application Signature and add it to the developer dashboard at https://developer.uber" + + ".com/dashboard"); + } else { + Log.e(UberSdk.UBER_SDK_LOG_TAG, "Your Application Signature, " + appSignature + + ", does not match one of the registered Application Signatures on the developer dashboard. " + + "Check your settings at https://developer.uber.com/dashboard"); } + } + callback.onLoginError(authenticationError); + } - try { - AccessToken token = AuthUtils.parseTokenUri(fragmentUri); - accessTokenStorage.setAccessToken(token); - callback.onLoginSuccess(token); - } catch (LoginAuthenticationException e) { - callback.onLoginError(e.getAuthenticationError()); - } - } else if(data.getStringExtra(EXTRA_ERROR) != null) { - final String error = data.getStringExtra(EXTRA_ERROR); - final AuthenticationError authenticationError - = (error != null) ? AuthenticationError.fromString(error) : AuthenticationError.UNKNOWN; - - if (authenticationError.equals(AuthenticationError.CANCELLED)) { - // User canceled login - callback.onLoginCancel(); - return; - } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) && - !AuthUtils.isPrivilegeScopeRequired(sessionConfiguration.getScopes())) { - loginForImplicitGrant(activity); - return; - } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) - && redirectForAuthorizationCode) { - loginForAuthorizationCode(activity); - } else if (AuthenticationError.INVALID_APP_SIGNATURE.equals(authenticationError)) { - AppProtocol appProtocol = new AppProtocol(); - String appSignature = appProtocol.getAppSignature(activity); - if (appSignature == null) { - Log.e(UberSdk.UBER_SDK_LOG_TAG, "There was an error obtaining your Application Signature. Please check " - + "your Application Signature and add it to the developer dashboard at https://developer.uber" - + ".com/dashboard"); - } else { - Log.e(UberSdk.UBER_SDK_LOG_TAG, "Your Application Signature, " + appSignature - + ", does not match one of the registered Application Signatures on the developer dashboard. " - + "Check your settings at https://developer.uber.com/dashboard"); - } - } - callback.onLoginError(authenticationError); - } else if(data.getStringExtra(EXTRA_CODE_RECEIVED) != null) { - final String authorizationCode = data.getStringExtra(EXTRA_CODE_RECEIVED); + private void handleResultOk(@Nullable Intent data) { + if (data == null) { + // Unknown error, should never occur + callback.onLoginError(AuthenticationError.UNKNOWN); + return; + } + + final String authorizationCode = data.getStringExtra(EXTRA_CODE_RECEIVED); + if (authorizationCode != null) { callback.onAuthorizationCodeReceived(authorizationCode); - } else if (data.getStringExtra(LoginManager.EXTRA_ACCESS_TOKEN) != null) { + } else { AccessToken accessToken = AuthUtils.createAccessToken(data); accessTokenStorage.setAccessToken(accessToken); - callback.onLoginSuccess(accessToken); - } - } + callback.onLoginSuccess(accessToken); - private void loginWithCustomtab(@NonNull final Activity activity, @NonNull Uri uri) { - final CustomTabsIntent intent = new CustomTabsIntent.Builder().build(); - CustomTabsHelper.openCustomTab(activity, intent, uri, new CustomTabsHelper - .BrowserFallback()); + } } private void validateRedirectUriRegistration(@NonNull Activity activity) { - if (!AuthUtils.isRedirectUriRegistered(activity, Uri.parse(sessionConfiguration - .getRedirectUri()))) { - String error = "Must now register redirect_uri " + sessionConfiguration - .getRedirectUri() + " in an intent filter in the AndroidManifest.xml of the " - + "application. See README in the rides-android-sdk for more information."; + String generatedRedirectUri = activity.getPackageName().concat("uberauth"); + String setRedirectUri = sessionConfiguration.getRedirectUri(); + if (setRedirectUri != null && + !generatedRedirectUri.equals(setRedirectUri) + && !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri))) { + String error = "Misconfigured redirect_uri in " + sessionConfiguration + .getRedirectUri() + " and the LoginRedirectReceiverActivity to receive the " + + "response. See https://github.com/uber/rides-android-sdk for more info."; if (Utility.isDebugable(activity)) { throw new IllegalStateException(error); diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/SsoDeeplink.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/SsoDeeplink.java index 7d8280fd..25b3f204 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/SsoDeeplink.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/SsoDeeplink.java @@ -61,14 +61,12 @@ public class SsoDeeplink implements Deeplink { private static final String URI_QUERY_PLATFORM = "sdk"; private static final String URI_QUERY_SDK_VERSION = "sdk_version"; private static final String URI_HOST = "connect"; - private static final String URI_QUERY_REDIRECT_URI = "redirect_uri"; private final Activity activity; private final String clientId; private final Collection requestedScopes; private final Collection requestedCustomScopes; private final int requestCode; - private final String redirectUri; AppProtocol appProtocol; @@ -77,14 +75,12 @@ public class SsoDeeplink implements Deeplink { @NonNull String clientId, @NonNull Collection requestedScopes, @NonNull Collection requestedCustomScopes, - int requestCode, - @NonNull String redirectUri) { + int requestCode) { this.activity = activity; this.clientId = clientId; this.requestCode = requestCode; this.requestedScopes = requestedScopes; this.requestedCustomScopes = requestedCustomScopes; - this.redirectUri = redirectUri; appProtocol = new AppProtocol(); } @@ -116,8 +112,8 @@ public void execute() { private Uri createSsoUri() { String scopes = AuthUtils.scopeCollectionToString(requestedScopes); if (!requestedCustomScopes.isEmpty()) { - scopes = AuthUtils.mergeScopeStrings(scopes, - AuthUtils.customScopeCollectionToString(requestedCustomScopes)); + scopes = AuthUtils.mergeScopeStrings(scopes, + AuthUtils.customScopeCollectionToString(requestedCustomScopes)); } return new Uri.Builder().scheme(AppProtocol.DEEPLINK_SCHEME) .authority(URI_HOST) @@ -125,7 +121,6 @@ private Uri createSsoUri() { .appendQueryParameter(URI_QUERY_SCOPE, scopes) .appendQueryParameter(URI_QUERY_PLATFORM, AppProtocol.PLATFORM) .appendQueryParameter(URI_QUERY_SDK_VERSION, BuildConfig.VERSION_NAME) - .appendQueryParameter(URI_QUERY_REDIRECT_URI, redirectUri) .build(); } @@ -158,7 +153,6 @@ public static class Builder { private Collection requestedScopes; private Collection requestedCustomScopes; private int requestCode = DEFAULT_REQUEST_CODE; - private String redirectUri; public Builder(@NonNull Activity activity) { this.activity = activity; @@ -189,15 +183,8 @@ public Builder activityRequestCode(int requestCode) { return this; } - - public Builder redirectUri(@NonNull String redirectUri) { - this.redirectUri = redirectUri; - return this; - } - public SsoDeeplink build() { checkNotNull(clientId, "Client Id must be set"); - checkNotNull(redirectUri, "redirectUri must be set"); checkNotEmpty(requestedScopes, "Scopes must be set."); @@ -208,8 +195,7 @@ public SsoDeeplink build() { if (requestCode == DEFAULT_REQUEST_CODE) { Log.i(UBER_SDK_LOG_TAG, "Request code is not set, using default request code"); } - return new SsoDeeplink(activity, clientId, requestedScopes, requestedCustomScopes, - requestCode, redirectUri); + return new SsoDeeplink(activity, clientId, requestedScopes, requestedCustomScopes, requestCode); } } } diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/AuthUtilsTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/AuthUtilsTest.java index 69d0bc17..550e3468 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/AuthUtilsTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/AuthUtilsTest.java @@ -27,10 +27,12 @@ import com.uber.sdk.android.core.RobolectricTestBase; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.Scope; +import com.uber.sdk.core.client.SessionConfiguration; import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static junit.framework.Assert.assertEquals; @@ -260,4 +262,21 @@ public void getCodeFromUrl_whenNoValidAuthorizationCodePassed() throws LoginAuth public void testCreateEncodedParam() { assertThat(AuthUtils.createEncodedParam("{\"redirect_to_login\":true}")).isEqualTo("eyJyZWRpcmVjdF90b19sb2dpbiI6dHJ1ZX0=\n"); } + + @Test + public void onBuildUrl_withDefaultRegion_shouldHaveDefaultUberDomain() { + String clientId = "clientId1234"; + String redirectUri = "localHost1234"; + + SessionConfiguration loginConfiguration = new SessionConfiguration.Builder() + .setRedirectUri(redirectUri) + .setScopes(Arrays.asList(Scope.HISTORY)) + .setClientId(clientId) + .build(); + + String url = AuthUtils.buildUrl(redirectUri, ResponseType.TOKEN, loginConfiguration); + assertEquals("https://login.uber.com/oauth/v2/authorize?client_id=" + clientId + + "&redirect_uri=" + redirectUri + "&response_type=token&scope=history&" + + "show_fb=false&signup_params=eyJyZWRpcmVjdF90b19sb2dpbiI6dHJ1ZX0%3D%0A", url); + } } diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java index 532e61f3..08fbd45f 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java @@ -131,8 +131,7 @@ public void testButtonClickWithoutLoginManager_shouldCreateNew() { .build(); loginButton = new LoginButton(activity, attributeSet); - loginButton.setSessionConfiguration(new SessionConfiguration.Builder().setRedirectUri - ("app://redirecturi").setClientId("clientId").build()); + loginButton.setSessionConfiguration(new SessionConfiguration.Builder().setClientId("clientId").build()); loginButton.setCallback(loginCallback); loginButton.setScopes(SCOPES); loginButton.setAccessTokenStorage(accessTokenStorage); diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java index d4664cd8..5d0e53f4 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java @@ -100,7 +100,6 @@ public class LoginManagerTest extends RobolectricTestBase { String.format("https://m.uber.com/sign-up?client_id=Client1234&user-agent=core-android-v%s-login_manager", BuildConfig.VERSION_NAME); private static final String AUTHORIZATION_CODE = "Auth123Code"; - private static final String REDIRECT_URI = "app://redirecturi"; @Mock Activity activity; @@ -120,8 +119,7 @@ public class LoginManagerTest extends RobolectricTestBase { @Before public void setup() { - sessionConfiguration = new SessionConfiguration.Builder().setClientId(CLIENT_ID) - .setScopes(MIXED_SCOPES).setRedirectUri(REDIRECT_URI).build(); + sessionConfiguration = new SessionConfiguration.Builder().setClientId(CLIENT_ID).setScopes(MIXED_SCOPES).build(); loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration); when(activity.getPackageManager()).thenReturn(packageManager); @@ -234,6 +232,12 @@ public void onActivityResult_whenResultOkAndHasCode_shouldCallbackSuccess() { assertThat(capturedCode.getValue()).isEqualTo(AUTHORIZATION_CODE); } + @Test + public void onActivityResult_whenResultCanceledAndNoData_shouldCallbackCancel() { + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); + verify(callback).onLoginCancel(); + } + @Test public void onActivityResult_whenResultCanceledAndHasData_shouldCallbackError() { Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.INVALID_RESPONSE @@ -244,12 +248,32 @@ public void onActivityResult_whenResultCanceledAndHasData_shouldCallbackError() } @Test - public void onActivityResult_whenResultCanceledAndDataButNoCallback_nothingShouldHappen() { + public void onActivityResult_whenResultCanceledAndNoData_shouldCancel() { + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); + verify(callback).onLoginCancel(); + } + + @Test + public void onActivityResult_whenResultOkAndNoData_shouldCallbackErrorUnknown() { + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, null); + verify(callback).onLoginError(AuthenticationError.UNKNOWN); + } + + @Test + public void onActivityResult_whenRequestCodeDoesNotMatch_nothingShouldHappen() { Intent intent = mock(Intent.class); loginManager.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); + verifyZeroInteractions(intent); verifyZeroInteractions(callback); } + @Test + public void onActivityResult_whenResultCanceledAndDataButNoCallback_nothingShouldHappen() { + Intent intent = mock(Intent.class); + loginManager.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); + verifyZeroInteractions(intent); + } + @Test public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerAuthorizationCode() { Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.UNAVAILABLE.toStandardString()); diff --git a/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java b/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java index c49c282e..8964c311 100644 --- a/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java +++ b/rides-android/src/main/java/com/uber/sdk/android/rides/RideRequestActivity.java @@ -41,7 +41,6 @@ import com.uber.sdk.android.core.auth.AuthenticationError; import com.uber.sdk.android.core.auth.LoginCallback; import com.uber.sdk.android.core.auth.LoginManager; -import com.uber.sdk.android.core.auth.ResponseType; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.AccessTokenStorage; import com.uber.sdk.core.auth.Scope; @@ -95,9 +94,9 @@ public class RideRequestActivity extends Activity implements LoginCallback, Ride */ @NonNull public static Intent newIntent(@NonNull Context context, - @Nullable RideParameters rideParameters, - @NonNull SessionConfiguration loginConfiguration, - @Nullable String accessTokenStorageKey) { + @Nullable RideParameters rideParameters, + @NonNull SessionConfiguration loginConfiguration, + @Nullable String accessTokenStorageKey) { Intent data = new Intent(context, RideRequestActivity.class); if (rideParameters == null) { @@ -156,7 +155,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == LOGIN_REQUEST_CODE) { - loginManager.handleAuthorizationResult(this, data); + loginManager.onActivityResult(this, requestCode, resultCode, data); } } @@ -254,7 +253,7 @@ private void load() { } private void login() { - loginManager.loginWithWebView(this, ResponseType.TOKEN); + loginManager.login(this); } /** diff --git a/rides-android/src/test/java/com/uber/sdk/android/rides/RideRequestActivityTest.java b/rides-android/src/test/java/com/uber/sdk/android/rides/RideRequestActivityTest.java index 0a808452..19d7a58d 100644 --- a/rides-android/src/test/java/com/uber/sdk/android/rides/RideRequestActivityTest.java +++ b/rides-android/src/test/java/com/uber/sdk/android/rides/RideRequestActivityTest.java @@ -30,7 +30,6 @@ import com.uber.sdk.android.core.auth.AccessTokenManager; import com.uber.sdk.android.core.auth.AuthenticationError; import com.uber.sdk.android.core.auth.LoginManager; -import com.uber.sdk.android.core.auth.ResponseType; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.AccessTokenStorage; import com.uber.sdk.core.auth.Scope; @@ -48,7 +47,6 @@ import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; import static org.mockito.Matchers.refEq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -201,7 +199,7 @@ public void onLoad_whenRideRequestViewAuthorizationErrorOccurs_shouldAttemptLogi assertNull(shadowActivity.getResultIntent()); verify(activity.accessTokenStorage, times(1)).removeAccessToken(); - verify(activity.loginManager).loginWithWebView(refEq(activity), eq(ResponseType.TOKEN)); + verify(activity.loginManager).login(refEq(activity)); } @Test diff --git a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java index 927b67a9..d66c1144 100644 --- a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java +++ b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java @@ -41,7 +41,6 @@ import com.uber.sdk.android.core.auth.LoginButton; import com.uber.sdk.android.core.auth.LoginCallback; import com.uber.sdk.android.core.auth.LoginManager; -import com.uber.sdk.android.core.auth.ResponseType; import com.uber.sdk.android.rides.samples.BuildConfig; import com.uber.sdk.android.rides.samples.R; import com.uber.sdk.core.auth.AccessToken; @@ -91,6 +90,7 @@ public class LoginSampleActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sample); + configuration = new SessionConfiguration.Builder() .setClientId(CLIENT_ID) .setRedirectUri(REDIRECT_URI) @@ -104,15 +104,15 @@ protected void onCreate(Bundle savedInstanceState) { //Create a button with a custom request code whiteButton = (LoginButton) findViewById(R.id.uber_button_white); whiteButton.setCallback(new SampleLoginCallback()) - .setSessionConfiguration(configuration); + .setSessionConfiguration(configuration); //Create a button using a custom AccessTokenStorage //Custom Scopes are set using XML for this button as well in R.layout.activity_sample blackButton = (LoginButton) findViewById(R.id.uber_button_black); blackButton.setAccessTokenStorage(accessTokenStorage) - .setCallback(new SampleLoginCallback()) - .setSessionConfiguration(configuration) - .setRequestCode(LOGIN_BUTTON_CUSTOM_REQUEST_CODE); + .setCallback(new SampleLoginCallback()) + .setSessionConfiguration(configuration) + .setRequestCode(LOGIN_BUTTON_CUSTOM_REQUEST_CODE); //Use a custom button with an onClickListener to call the LoginManager directly @@ -120,13 +120,11 @@ protected void onCreate(Bundle savedInstanceState) { new SampleLoginCallback(), configuration, CUSTOM_BUTTON_REQUEST_CODE); - loginManager.handleAuthorizationResult(this, getIntent()); customButton = (Button) findViewById(R.id.custom_uber_button); customButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - loginManager.login(LoginSampleActivity.this); } }); @@ -145,8 +143,12 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(LOG_TAG, String.format("onActivityResult requestCode:[%s] resultCode [%s]", requestCode, resultCode)); - //A temporary measure to account for older Uber app SSO implementations in the wild - loginManager.handleAuthorizationResult(this, data); + //Allow each a chance to catch it. + whiteButton.onActivityResult(requestCode, resultCode, data); + + blackButton.onActivityResult(requestCode, resultCode, data); + + loginManager.onActivityResult(this, requestCode, resultCode, data); } private class SampleLoginCallback implements LoginCallback { @@ -182,21 +184,21 @@ private void loadProfileInfo() { service.getUserProfile() .enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - Toast.makeText(LoginSampleActivity.this, getString(R.string.greeting, response.body().getFirstName()), Toast.LENGTH_LONG).show(); - } else { - ApiError error = ErrorParser.parseError(response); - Toast.makeText(LoginSampleActivity.this, error.getClientErrors().get(0).getTitle(), Toast.LENGTH_LONG).show(); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - - } - }); + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(LoginSampleActivity.this, getString(R.string.greeting, response.body().getFirstName()), Toast.LENGTH_LONG).show(); + } else { + ApiError error = ErrorParser.parseError(response); + Toast.makeText(LoginSampleActivity.this, error.getClientErrors().get(0).getTitle(), Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + + } + }); } @Override From 68b0c51fdf77dc7d8ccab6a0c3d798d8f1812f1c Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Thu, 11 Jan 2018 17:37:43 -0800 Subject: [PATCH 08/19] Updating Readme --- README.md | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 6d984bd5..a5fc2472 100644 --- a/README.md +++ b/README.md @@ -201,35 +201,35 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data){ ``` #### Authentication Migration (Version 0.8 and above) +With Version 0.8 and above of the SDK, the redirect URI is more strongly enforced to meet IETF +standards [IETF RFC](https://tools.ietf.org/html/draft-ietf-oauth-native-apps-12). -We are moving the startActivityForResult/onActivityResult contract to use the standard URI -contract indicated in the [IETF RFC](https://tools.ietf.org/html/draft-ietf-oauth-native-apps-12). +The SDK will automatically created a redirect URI to be used in the oauth callbacks with +the format "applicationId.uberauth", ex "com.example.uberauth". If this differs from the previous +specified redirect URI configured in the SessionConfiguration, there are two options. -Now, you must additionally specify your redirect_uri in the Android Manifest and -proxy that information to the Uber SDK. The recommended format is com.example.yourpackage://redirect-uri + 1. Change the redirect URI to match the new scheme in the configuration of the Session. If this is left out entirely, the default will be used. -To handle migrations from older Uber apps using the old contract, we recommend you implement both -approaches in the -short term until we indicate otherwise. - -```xml - - - - - - +```java +SessionConfiguration config = new SessionConfiguration.Builder() + .setRedirectUri("com.example.app.uberauth") + .build(); ``` -Following this change, you must proxy the results to the `LoginManager` from the called activity. + 2. Override the LoginRedirectReceiverActivity in your main manifest and provide a custom intent +filter. -```java -@Override -protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - loginManager.handleAuthorizationResult(intent); -} +```xml + + + + + + + + ``` The default behavior of calling `LoginManager.login(activity)` is to activate Single Sign On, and if that is unavailable, fallback to Implicit Grant if privileged scopes are not requested, otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager.setRedirectForAuthorizationCode(true)` to prevent the redirect to the Play Store. Implicit Grant will allow access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes). From 0e487a1b243f3e12731663b14794a12c5598d69a Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Thu, 11 Jan 2018 17:50:17 -0800 Subject: [PATCH 09/19] Fixing fallbacks --- .../com/uber/sdk/android/core/auth/LoginActivity.java | 5 ++++- .../com/uber/sdk/android/core/auth/LoginManager.java | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java index b634bc83..1a7761ac 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java @@ -169,8 +169,11 @@ protected void loadUrl() { String redirectUri = sessionConfiguration.getRedirectUri() != null ? sessionConfiguration .getRedirectUri() : getApplicationContext().getPackageName() + "uberauth"; + boolean registeredRedirectUri = AuthUtils.isRedirectUriRegistered(this, + Uri.parse(redirectUri)); + String url = AuthUtils.buildUrl(redirectUri, responseType, sessionConfiguration); - if (getIntent().getBooleanExtra(EXTRA_FORCE_WEBVIEW, false)) { + if (getIntent().getBooleanExtra(EXTRA_FORCE_WEBVIEW, false) || !registeredRedirectUri) { loadWebview(url, redirectUri); } else { loadChrometab(url); diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index 93110a9d..8c4f20f2 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -368,14 +368,17 @@ private void handleResultOk(@Nullable Intent data) { private void validateRedirectUriRegistration(@NonNull Activity activity) { - String generatedRedirectUri = activity.getPackageName().concat("uberauth"); + String generatedRedirectUri = activity.getPackageName().concat(".uberauth://redirect"); String setRedirectUri = sessionConfiguration.getRedirectUri(); if (setRedirectUri != null && !generatedRedirectUri.equals(setRedirectUri) && !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri))) { - String error = "Misconfigured redirect_uri in " + sessionConfiguration - .getRedirectUri() + " and the LoginRedirectReceiverActivity to receive the " - + "response. See https://github.com/uber/rides-android-sdk for more info."; + String error = "Misconfigured redirect_uri. See https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above " + + "for more info. Either 1) Register " + generatedRedirectUri + " as a " + + "redirect uri for the app at https://developer.uber.com/dashboard/ and " + + "specify this in your SessionConfiguration or 2) Override the default " + + "redirect_uri with the current one set (" + setRedirectUri + ") in the " + + "AndroidManifest."; if (Utility.isDebugable(activity)) { throw new IllegalStateException(error); From 3cdd4ccf0ce2ecf55307f4d8480b91eb5c9a2c06 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Tue, 16 Jan 2018 15:42:36 -0800 Subject: [PATCH 10/19] Resolving broken unit tests --- .../java/com/uber/sdk/android/core/auth/LoginManagerTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java index 5d0e53f4..7b937b27 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java @@ -124,6 +124,7 @@ public void setup() { when(activity.getPackageManager()).thenReturn(packageManager); when(activity.getApplicationInfo()).thenReturn(new ApplicationInfo()); + when(activity.getPackageName()).thenReturn("com.example"); } @Test From bc86bf62fb674d90f4af23935754449485d94f00 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Wed, 24 Jan 2018 13:37:10 -0800 Subject: [PATCH 11/19] Addressing feedback --- .../android/core/auth/CustomTabsHelper.java | 26 ++++++------------- .../sdk/android/core/auth/LoginManager.java | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java index 85d11254..2b287699 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/CustomTabsHelper.java @@ -30,6 +30,8 @@ import android.text.TextUtils; import android.util.Log; +import com.uber.sdk.android.core.UberSdk; + import java.util.ArrayList; import java.util.List; @@ -66,11 +68,7 @@ public static void openCustomTab( CustomTabFallback fallback) { final String packageName = getPackageNameToUse(activity); - if (packageName == null) { - if (fallback != null) { - fallback.openUri(activity, uri); - } - } else { + if (packageName != null) { final CustomTabsServiceConnection connection = new CustomTabsServiceConnection() { @Override public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) { @@ -84,20 +82,12 @@ public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabs public void onServiceDisconnected(ComponentName name) {} }; CustomTabsClient.bindCustomTabsService(activity, packageName, connection); + } else if (fallback != null) { + fallback.openUri(activity, uri); + } else { + Log.e(UberSdk.UBER_SDK_LOG_TAG, + "Use of openCustomTab without Customtab support or a fallback set"); } -// customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | -// Intent.FLAG_ACTIVITY_CLEAR_TASK); -// customTabsIntent.intent.setPackage(packageName); -// customTabsIntent.intent.setData(uri); -// activity.startActivityForResult(customTabsIntent.intent, 20); -// customTabsIntent.launchUrl(activity, uri); - -// } - } - - - static void launchTab(final Context context, final Uri uri){ - } /** diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index 8c4f20f2..68fd4d97 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -383,7 +383,7 @@ private void validateRedirectUriRegistration(@NonNull Activity activity) { if (Utility.isDebugable(activity)) { throw new IllegalStateException(error); } else { - Log.e("Uber SDK", error); + Log.e(UberSdk.UBER_SDK_LOG_TAG, error); } } } From 6ab807f9d8768de55f2943d5c86a7a068315cf2f Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Wed, 24 Jan 2018 15:06:18 -0800 Subject: [PATCH 12/19] Improving Readme instructions for custom tabs --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a5fc2472..9ed2f4ef 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,18 @@ If you want to provide a more custom experience in your app, there are a few cla ### Login The Uber SDK allows for three login flows: Implicit Grant (local web view), Single Sign On with the Uber App, and Authorization Code Grant (requires a backend to catch the local web view redirect and complete OAuth). -To use Single Sign On you must register a hash of your application's signing certificate in the Application Signature section of the [developer dashboard](https://developer.uber.com/dashboard). + +#### Dashboard configuration +To use SDK features, two configuration details must be set on the Uber Developer Dashboard. + + 1. Sign into to the [developer dashboard](https://developer.uber.com/dashboard) + + 1. Register a redirect URI to be used to communication authentication results. The default used + by the SDK is in the format of `applicationId.uberauth://redirect`. ex: `com.example + .uberauth://redirect`. To configure the SDK to use a different redirect URI, see the steps below. + + 1. To use Single Sign On you must register a hash of your application's signing certificate in the + Application Signature section of the settings page of your application. To get the hash of your signing certificate, run this command with the alias of your key and path to your keystore: @@ -200,7 +211,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data){ } ``` -#### Authentication Migration (Version 0.8 and above) +#### Authentication Migration and setup (Version 0.8 and above) With Version 0.8 and above of the SDK, the redirect URI is more strongly enforced to meet IETF standards [IETF RFC](https://tools.ietf.org/html/draft-ietf-oauth-native-apps-12). @@ -232,7 +243,9 @@ filter. ``` -The default behavior of calling `LoginManager.login(activity)` is to activate Single Sign On, and if that is unavailable, fallback to Implicit Grant if privileged scopes are not requested, otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager.setRedirectForAuthorizationCode(true)` to prevent the redirect to the Play Store. Implicit Grant will allow access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes). +The default behavior of calling `LoginManager.login(activity)` is to activate Single Sign On, +and if SSO is unavailable, fallback to Implicit Grant if privileged scopes are not requested, +otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager.setRedirectForAuthorizationCode(true)` to prevent the redirect to the Play Store. Implicit Grant will allow access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes). #### Login Errors Upon a failure to login, an `AuthenticationError` will be provided in the `LoginCallback`. This enum provides a series of values that provide more information on the type of error. From 06ebd6f7e748e3964d05442f3b63579bd48aaa9c Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Mon, 29 Jan 2018 11:50:05 -0800 Subject: [PATCH 13/19] Adding additional error check flag related to auth code flow --- .../main/java/com/uber/sdk/android/core/auth/LoginManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index 68fd4d97..083feb7e 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -370,7 +370,8 @@ private void validateRedirectUriRegistration(@NonNull Activity activity) { String generatedRedirectUri = activity.getPackageName().concat(".uberauth://redirect"); String setRedirectUri = sessionConfiguration.getRedirectUri(); - if (setRedirectUri != null && + if (isRedirectForAuthorizationCode() && + setRedirectUri != null && !generatedRedirectUri.equals(setRedirectUri) && !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri))) { String error = "Misconfigured redirect_uri. See https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above " From ec47377f05b6bed66a321dd2f2c9a3b5fc1c2d61 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Mon, 5 Feb 2018 15:27:43 -0800 Subject: [PATCH 14/19] Moving migration logic to better account for auth code flow --- .../sdk/android/core/auth/LoginActivity.java | 5 +- .../sdk/android/core/auth/LoginCallback.java | 2 + .../sdk/android/core/auth/LoginManager.java | 108 +++++++++++--- .../android/core/auth/LoginButtonTest.java | 4 +- .../android/core/auth/LoginManagerTest.java | 137 +++++++++++++----- 5 files changed, 194 insertions(+), 62 deletions(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java index 1a7761ac..b634bc83 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginActivity.java @@ -169,11 +169,8 @@ protected void loadUrl() { String redirectUri = sessionConfiguration.getRedirectUri() != null ? sessionConfiguration .getRedirectUri() : getApplicationContext().getPackageName() + "uberauth"; - boolean registeredRedirectUri = AuthUtils.isRedirectUriRegistered(this, - Uri.parse(redirectUri)); - String url = AuthUtils.buildUrl(redirectUri, responseType, sessionConfiguration); - if (getIntent().getBooleanExtra(EXTRA_FORCE_WEBVIEW, false) || !registeredRedirectUri) { + if (getIntent().getBooleanExtra(EXTRA_FORCE_WEBVIEW, false)) { loadWebview(url, redirectUri); } else { loadChrometab(url); diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginCallback.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginCallback.java index 5dc556cb..a8bc569b 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginCallback.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginCallback.java @@ -56,6 +56,8 @@ public interface LoginCallback { * Client Secret, see https://developer.uber.com/docs/authentication#section-step-two-receive-redirect * * @param authorizationCode the authorizationCode that can be used to retrieve {@link AccessToken} + * @deprecated Should use Custom Tab flow with registered Redirect URI */ + @Deprecated void onAuthorizationCodeReceived(@NonNull String authorizationCode); } diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index 083feb7e..cf9892e1 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -43,6 +43,7 @@ import com.uber.sdk.core.client.SessionConfiguration; import static com.uber.sdk.core.client.utils.Preconditions.checkNotEmpty; +import static com.uber.sdk.core.client.utils.Preconditions.checkNotNull; /** * Manages user login via OAuth 2.0 Implicit Grant. Be sure to call @@ -96,8 +97,11 @@ public class LoginManager { private final SessionConfiguration sessionConfiguration; private final int requestCode; + private boolean authCodeFlowEnabled = false; + @Deprecated private boolean redirectForAuthorizationCode = false; + /** * @param accessTokenStorage to store access token. * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} @@ -146,6 +150,8 @@ public LoginManager( public void login(@NonNull Activity activity) { checkNotEmpty(sessionConfiguration.getScopes(), "Scopes must be set in the Session " + "Configuration."); + checkNotNull(sessionConfiguration.getRedirectUri(), "Redirect URI must be set in " + + "Session Configuration."); SsoDeeplink ssoDeeplink = new SsoDeeplink.Builder(activity) .clientId(sessionConfiguration.getClientId()) @@ -158,7 +164,7 @@ public void login(@NonNull Activity activity) { ssoDeeplink.execute(); } else if (!AuthUtils.isPrivilegeScopeRequired(sessionConfiguration.getScopes())) { loginForImplicitGrant(activity); - } else if (redirectForAuthorizationCode) { + } else if (isAuthCodeFlowEnabled()) { loginForAuthorizationCode(activity); } else { redirectToInstallApp(activity); @@ -171,8 +177,11 @@ public void login(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForImplicitGrant(@NonNull Activity activity) { - validateRedirectUriRegistration(activity); - Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, ResponseType.TOKEN); + boolean forceWebView = + isInLegacyRedirectMode(activity); + + Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, + ResponseType.TOKEN, forceWebView); activity.startActivityForResult(intent, requestCode); } @@ -182,8 +191,10 @@ public void loginForImplicitGrant(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForAuthorizationCode(@NonNull Activity activity) { - validateRedirectUriRegistration(activity); - Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, ResponseType.CODE); + boolean forceWebView = + isInLegacyRedirectMode(activity); + Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, + ResponseType.CODE, forceWebView); activity.startActivityForResult(intent, requestCode); } @@ -256,9 +267,12 @@ public boolean isAuthenticated() { * * @param redirectForAuthorizationCode true if should redirect, otherwise false * @return this instance of {@link LoginManager} + * @deprecated, See Authorization Migration guide https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above */ + @Deprecated public LoginManager setRedirectForAuthorizationCode(boolean redirectForAuthorizationCode) { this.redirectForAuthorizationCode = redirectForAuthorizationCode; + setAuthCodeFlowEnabled(true); return this; } @@ -272,11 +286,42 @@ public LoginManager setRedirectForAuthorizationCode(boolean redirectForAuthoriza * update Uber app instead. * * @return true if redirect by default, otherwise false + * @deprecated, See Authorization Migration guide https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above */ + @Deprecated public boolean isRedirectForAuthorizationCode() { return redirectForAuthorizationCode; } + + /** + * Enable the use of the Authorization Code Flow + * (https://developer.uber.com/docs/authentication#section-step-one-authorize) instead of an + * installation prompt for the Uber app as a login fallback mechanism. + * + * Requires that the app's backend system is configured to support this flow and the redirect + * URI is pointed correctly. + * + * @param authCodeFlowEnabled true for use of auth code flow, false to fallback to Uber app + * installation + * @return this instace of {@link LoginManager} + */ + public LoginManager setAuthCodeFlowEnabled(boolean authCodeFlowEnabled) { + this.authCodeFlowEnabled = authCodeFlowEnabled; + return this; + } + + /** + * Indicates the use of the Authorization Code Flow + * (https://developer.uber.com/docs/authentication#section-step-one-authorize) instead of an + * installation prompt for the Uber app as a login fallback mechanism. + * + * @return true if Auth Code Flow is enabled, otherwise false + */ + public boolean isAuthCodeFlowEnabled() { + return authCodeFlowEnabled; + } + private void redirectToInstallApp(@NonNull Activity activity) { new SignupDeeplink(activity, sessionConfiguration.getClientId(), USER_AGENT).execute(); } @@ -329,7 +374,7 @@ private void handleResultCancelled( loginForImplicitGrant(activity); return; } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) - && redirectForAuthorizationCode) { + && isAuthCodeFlowEnabled()) { loginForAuthorizationCode(activity); } else if (AuthenticationError.INVALID_APP_SIGNATURE.equals(authenticationError)) { AppProtocol appProtocol = new AppProtocol(); @@ -366,26 +411,53 @@ private void handleResultOk(@Nullable Intent data) { } } - private void validateRedirectUriRegistration(@NonNull Activity activity) { - + boolean isInLegacyRedirectMode(@NonNull Activity activity) { String generatedRedirectUri = activity.getPackageName().concat(".uberauth://redirect"); String setRedirectUri = sessionConfiguration.getRedirectUri(); - if (isRedirectForAuthorizationCode() && - setRedirectUri != null && - !generatedRedirectUri.equals(setRedirectUri) - && !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri))) { - String error = "Misconfigured redirect_uri. See https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above " + + if (redirectForAuthorizationCode) { + String message = "The Uber Authentication Flow for the Authorization Code Flow has " + + "been upgraded in 0.8.0 and a redirect URI must now be supplied to the application. " + + "You are seeing this error because the use of deprecated method " + + "LoginManager.setRedirectForAuthorizationCode() indicates your flow may not " + + "support the recent changes. See https://github" + + ".com/uber/rides-android-sdk#authentication-migration-version" + + "-08-and-above for resolution steps" + + "to insure your setup is correct and then migrate to the non-deprecate " + + "method LoginManager.setAuthCodeFlowEnabled()"; + + logOrError(activity, new IllegalStateException(message)); + return true; + } + + if (sessionConfiguration.getRedirectUri() == null) { + String message = "Redirect URI must be set in " + + "Session Configuration."; + + logOrError(activity, new NullPointerException(message)); + return true; + } + + if (!generatedRedirectUri.equals(setRedirectUri) && + !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri))) { + String message = "Misconfigured redirect_uri. See https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above" + "for more info. Either 1) Register " + generatedRedirectUri + " as a " + "redirect uri for the app at https://developer.uber.com/dashboard/ and " + "specify this in your SessionConfiguration or 2) Override the default " + "redirect_uri with the current one set (" + setRedirectUri + ") in the " + "AndroidManifest."; + logOrError(activity, new IllegalStateException(message)); + return true; + } - if (Utility.isDebugable(activity)) { - throw new IllegalStateException(error); - } else { - Log.e(UberSdk.UBER_SDK_LOG_TAG, error); - } + return false; + } + + private void logOrError(Activity activity, Exception exception) { + if (Utility.isDebugable(activity)) { + throw new IllegalStateException(exception); + } else { + Log.e(UberSdk.UBER_SDK_LOG_TAG, exception.getMessage(), exception); } } } diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java index 08fbd45f..7fdc1c71 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginButtonTest.java @@ -131,7 +131,9 @@ public void testButtonClickWithoutLoginManager_shouldCreateNew() { .build(); loginButton = new LoginButton(activity, attributeSet); - loginButton.setSessionConfiguration(new SessionConfiguration.Builder().setClientId("clientId").build()); + loginButton.setSessionConfiguration(new SessionConfiguration.Builder() + .setRedirectUri("com.example://redirect") + .setClientId("clientId").build()); loginButton.setCallback(loginCallback); loginButton.setScopes(SCOPES); loginButton.setAccessTokenStorage(accessTokenStorage); diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java index 7b937b27..0f2b767a 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java @@ -113,25 +113,46 @@ public class LoginManagerTest extends RobolectricTestBase { @Mock AccessTokenStorage accessTokenStorage; - SessionConfiguration sessionConfiguration; + SessionConfiguration SessionConfigurationWithSetUri; + SessionConfiguration SessionConfigurationWithGeneratedUri; - private LoginManager loginManager; + private LoginManager loginManagerWithSetUri; + private LoginManager loginManagerwithGeneratedUri; + + private ApplicationInfo debuggableApplicationInfo; + private ApplicationInfo releaseApplicationInfo; @Before public void setup() { - sessionConfiguration = new SessionConfiguration.Builder().setClientId(CLIENT_ID).setScopes(MIXED_SCOPES).build(); - loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration); + SessionConfigurationWithGeneratedUri = new SessionConfiguration.Builder().setClientId(CLIENT_ID) + .setRedirectUri("com.example.uberauth://redirect") + .setScopes(MIXED_SCOPES).build(); + loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, + SessionConfigurationWithGeneratedUri); + + SessionConfigurationWithSetUri = new SessionConfiguration.Builder().setClientId(CLIENT_ID) + .setRedirectUri("com.custom://redirect") + .setScopes(MIXED_SCOPES).build(); + loginManagerWithSetUri = new LoginManager(accessTokenStorage, callback, + SessionConfigurationWithSetUri); when(activity.getPackageManager()).thenReturn(packageManager); when(activity.getApplicationInfo()).thenReturn(new ApplicationInfo()); when(activity.getPackageName()).thenReturn("com.example"); + + debuggableApplicationInfo = new ApplicationInfo(); + debuggableApplicationInfo.flags = ApplicationInfo.FLAG_DEBUGGABLE; + releaseApplicationInfo = new ApplicationInfo(); + releaseApplicationInfo.flags = 0; + when(activity.getApplicationInfo()).thenReturn(debuggableApplicationInfo); + } @Test public void loginWithAppInstalledPrivilegedScopes_shouldLaunchIntent() { stubAppInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0], SsoDeeplink.MIN_VERSION_SUPPORTED); - loginManager.login(activity); + loginManagerwithGeneratedUri.login(activity); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(Integer.class); @@ -144,11 +165,12 @@ public void loginWithAppInstalledPrivilegedScopes_shouldLaunchIntent() { @Test public void loginWithAppInstalledPrivilegedScopesAndRequestCode_shouldLaunchIntent() { - loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration, REQUEST_CODE); + loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, + SessionConfigurationWithGeneratedUri, REQUEST_CODE); stubAppInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0], SsoDeeplink.MIN_VERSION_SUPPORTED); - loginManager.login(activity); + loginManagerwithGeneratedUri.login(activity); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(Integer.class); @@ -161,12 +183,13 @@ public void loginWithAppInstalledPrivilegedScopesAndRequestCode_shouldLaunchInte @Test public void loginWithoutAppInstalledGeneralScopes_shouldLaunchWebView() { - sessionConfiguration = sessionConfiguration.newBuilder().setScopes(GENERAL_SCOPES).build(); - loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration); + SessionConfigurationWithGeneratedUri = SessionConfigurationWithGeneratedUri.newBuilder().setScopes(GENERAL_SCOPES).build(); + loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, + SessionConfigurationWithGeneratedUri); stubAppNotInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0]); - loginManager.login(activity); + loginManagerwithGeneratedUri.login(activity); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(Integer.class); @@ -185,13 +208,14 @@ public void loginWithoutAppInstalledPrivilegedScopes_shouldLaunchAppInstall() { final Activity activity = spy(Robolectric.setupActivity(Activity.class)); when(activity.getPackageManager()).thenReturn(packageManager); - sessionConfiguration = sessionConfiguration.newBuilder().build(); - loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration) - .setRedirectForAuthorizationCode(false); + SessionConfigurationWithGeneratedUri = SessionConfigurationWithGeneratedUri.newBuilder().build(); + loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, + SessionConfigurationWithGeneratedUri) + .setAuthCodeFlowEnabled(false); stubAppNotInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0]); - loginManager.login(activity); + loginManagerwithGeneratedUri.login(activity); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -209,7 +233,7 @@ public void onActivityResult_whenResultOkAndHasToken_shouldCallbackSuccess() { .putExtra(EXTRA_EXPIRES_IN, ACCESS_TOKEN.getExpiresIn()) .putExtra(EXTRA_TOKEN_TYPE, ACCESS_TOKEN.getTokenType()); - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, intent); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, intent); ArgumentCaptor storedToken = ArgumentCaptor.forClass(AccessToken.class); ArgumentCaptor returnedToken = ArgumentCaptor.forClass(AccessToken.class); @@ -225,7 +249,7 @@ public void onActivityResult_whenResultOkAndHasCode_shouldCallbackSuccess() { Intent intent = new Intent() .putExtra(EXTRA_CODE_RECEIVED, AUTHORIZATION_CODE); - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, intent); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, intent); ArgumentCaptor capturedCode = ArgumentCaptor.forClass(String.class); verify(callback).onAuthorizationCodeReceived(capturedCode.capture()); @@ -235,7 +259,7 @@ public void onActivityResult_whenResultOkAndHasCode_shouldCallbackSuccess() { @Test public void onActivityResult_whenResultCanceledAndNoData_shouldCallbackCancel() { - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); verify(callback).onLoginCancel(); } @@ -244,26 +268,26 @@ public void onActivityResult_whenResultCanceledAndHasData_shouldCallbackError() Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.INVALID_RESPONSE .toStandardString()); - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); verify(callback).onLoginError(AuthenticationError.INVALID_RESPONSE); } @Test public void onActivityResult_whenResultCanceledAndNoData_shouldCancel() { - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); verify(callback).onLoginCancel(); } @Test public void onActivityResult_whenResultOkAndNoData_shouldCallbackErrorUnknown() { - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, null); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, null); verify(callback).onLoginError(AuthenticationError.UNKNOWN); } @Test public void onActivityResult_whenRequestCodeDoesNotMatch_nothingShouldHappen() { Intent intent = mock(Intent.class); - loginManager.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); + loginManagerwithGeneratedUri.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); verifyZeroInteractions(intent); verifyZeroInteractions(callback); } @@ -271,7 +295,7 @@ public void onActivityResult_whenRequestCodeDoesNotMatch_nothingShouldHappen() { @Test public void onActivityResult_whenResultCanceledAndDataButNoCallback_nothingShouldHappen() { Intent intent = mock(Intent.class); - loginManager.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); + loginManagerwithGeneratedUri.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); verifyZeroInteractions(intent); } @@ -279,8 +303,8 @@ public void onActivityResult_whenResultCanceledAndDataButNoCallback_nothingShoul public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerAuthorizationCode() { Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.UNAVAILABLE.toStandardString()); - loginManager.setRedirectForAuthorizationCode(true); - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); + loginManagerwithGeneratedUri.setAuthCodeFlowEnabled(true); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -299,23 +323,25 @@ public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerAut @Test public void onActivityResult_whenUnavailableAndPrivilegedScopesNoRedirect_shouldError() { Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.UNAVAILABLE.toStandardString()); - sessionConfiguration = sessionConfiguration.newBuilder().build(); - loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration) - .setRedirectForAuthorizationCode(false); + SessionConfigurationWithGeneratedUri = SessionConfigurationWithGeneratedUri.newBuilder().build(); + loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, + SessionConfigurationWithGeneratedUri) + .setAuthCodeFlowEnabled(false); - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); verify(callback).onLoginError(AuthenticationError.UNAVAILABLE); } @Test public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerImplicitGrant() { - sessionConfiguration = sessionConfiguration.newBuilder().setScopes(GENERAL_SCOPES).build(); - loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration); + SessionConfigurationWithGeneratedUri = SessionConfigurationWithGeneratedUri.newBuilder().setScopes(GENERAL_SCOPES).build(); + loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, + SessionConfigurationWithGeneratedUri); Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.UNAVAILABLE.toStandardString()); - loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); + loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -334,41 +360,74 @@ public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerImp @Test public void isAuthenticated_withServerToken_true() { when(accessTokenStorage.getAccessToken()).thenReturn(null); - loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration.newBuilder().setServerToken("serverToken").build()); - assertTrue(loginManager.isAuthenticated()); + loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, SessionConfigurationWithGeneratedUri + .newBuilder().setServerToken("serverToken").build()); + assertTrue(loginManagerwithGeneratedUri.isAuthenticated()); } @Test public void isAuthenticated_withAccessToken_true() { when(accessTokenStorage.getAccessToken()).thenReturn(ACCESS_TOKEN); - assertTrue(loginManager.isAuthenticated()); + assertTrue(loginManagerwithGeneratedUri.isAuthenticated()); } @Test public void isAuthenticated_withoutAccessOrServerToken_false() { when(accessTokenStorage.getAccessToken()).thenReturn(null); - assertFalse(loginManager.isAuthenticated()); + assertFalse(loginManagerwithGeneratedUri.isAuthenticated()); } @Test public void getSession_withServerToken_successful() { when(accessTokenStorage.getAccessToken()).thenReturn(null); - loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration.newBuilder().setServerToken("serverToken").build()); - Session session = loginManager.getSession(); + loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, SessionConfigurationWithGeneratedUri + .newBuilder().setServerToken("serverToken").build()); + Session session = loginManagerwithGeneratedUri.getSession(); assertEquals("serverToken", session.getAuthenticator().getSessionConfiguration().getServerToken()); } @Test public void getSession_withAccessToken_successful() { when(accessTokenStorage.getAccessToken()).thenReturn(ACCESS_TOKEN); - Session session = loginManager.getSession(); + Session session = loginManagerwithGeneratedUri.getSession(); assertEquals(ACCESS_TOKEN, ((AccessTokenAuthenticator)session.getAuthenticator()).getTokenStorage().getAccessToken()); } @Test(expected = IllegalStateException.class) public void getSession_withoutAccessTokenOrToken_fails() { when(accessTokenStorage.getAccessToken()).thenReturn(null); - loginManager.getSession(); + loginManagerwithGeneratedUri.getSession(); + } + + @Test(expected = IllegalStateException.class) + public void isInLegacyRedirectMode_whenMismatchingUriInDebug_throwsException() { + loginManagerWithSetUri.isInLegacyRedirectMode(activity); + } + + @Test() + public void isInLegacyRedirectMode_whenMismatchingUriInRelease_logsErrorAndReturnsTrue() { + when(activity.getApplicationInfo()).thenReturn(releaseApplicationInfo); + assertThat(loginManagerWithSetUri.isInLegacyRedirectMode(activity)).isTrue(); + } + + + @Test(expected = IllegalStateException.class) + public void isInLegacyRedirectMode_whenLegacyAuthCodeFlowInDebug_throwsException() { + loginManagerwithGeneratedUri.setRedirectForAuthorizationCode(true); + loginManagerwithGeneratedUri.isInLegacyRedirectMode(activity); + + } + + @Test() + public void isInLegacyRedirectMode_whenLegacyAuthCodeFlowInRelease_logsErrorAndReturnsTrue() { + when(activity.getApplicationInfo()).thenReturn(releaseApplicationInfo); + loginManagerwithGeneratedUri.setRedirectForAuthorizationCode(true); + assertThat(loginManagerwithGeneratedUri.isInLegacyRedirectMode(activity)).isTrue(); + } + + @Test() + public void isInLegacyRedirectMode_whenMatchingUri_returnsFalse() { + assertThat(loginManagerwithGeneratedUri.isInLegacyRedirectMode(activity)).isFalse(); } private static PackageManager stubAppInstalled(PackageManager packageManager, String packageName, int versionCode) { From 4c73f008585d52f4379e18ac8378b35daf9fd09b Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Mon, 5 Feb 2018 17:06:08 -0800 Subject: [PATCH 15/19] Fix Auth Docs --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ed2f4ef..e0604605 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,9 @@ filter. The default behavior of calling `LoginManager.login(activity)` is to activate Single Sign On, and if SSO is unavailable, fallback to Implicit Grant if privileged scopes are not requested, -otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager.setRedirectForAuthorizationCode(true)` to prevent the redirect to the Play Store. Implicit Grant will allow access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes). +otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager +.setAuthCodeEnabled(true)` to prevent the redirect to the Play Store. Implicit Grant will allow +access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes). #### Login Errors Upon a failure to login, an `AuthenticationError` will be provided in the `LoginCallback`. This enum provides a series of values that provide more information on the type of error. From 8babeefbbca73cc2bc82c6c8f8bde8259cf145de Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Tue, 6 Feb 2018 11:46:08 -0800 Subject: [PATCH 16/19] cleanup --- .../sdk/android/core/auth/LoginManager.java | 9 +---- .../uber/sdk/android/core/utils/Utility.java | 13 +++++++ samples/login-sample/gradle.properties | 4 +- .../android/samples/LoginSampleActivity.java | 38 +++++++++---------- .../src/main/AndroidManifest.xml | 7 ---- 5 files changed, 35 insertions(+), 36 deletions(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index cf9892e1..c7bade13 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -42,6 +42,7 @@ import com.uber.sdk.core.client.Session; import com.uber.sdk.core.client.SessionConfiguration; +import static com.uber.sdk.android.core.utils.Utility.logOrError; import static com.uber.sdk.core.client.utils.Preconditions.checkNotEmpty; import static com.uber.sdk.core.client.utils.Preconditions.checkNotNull; @@ -452,12 +453,4 @@ boolean isInLegacyRedirectMode(@NonNull Activity activity) { return false; } - - private void logOrError(Activity activity, Exception exception) { - if (Utility.isDebugable(activity)) { - throw new IllegalStateException(exception); - } else { - Log.e(UberSdk.UBER_SDK_LOG_TAG, exception.getMessage(), exception); - } - } } diff --git a/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java b/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java index 25d58c4c..e4b606ed 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java @@ -1,8 +1,12 @@ package com.uber.sdk.android.core.utils; +import android.app.Activity; import android.content.Context; import android.content.pm.ApplicationInfo; import android.support.annotation.NonNull; +import android.util.Log; + +import com.uber.sdk.android.core.UberSdk; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -28,6 +32,15 @@ public static boolean isDebugable(@NonNull Context context) { } + + public static void logOrError(Activity activity, Exception exception) { + if (Utility.isDebugable(activity)) { + throw new IllegalStateException(exception); + } else { + Log.e(UberSdk.UBER_SDK_LOG_TAG, exception.getMessage(), exception); + } + } + private static String hashWithAlgorithm(String algorithm, String key) { return hashWithAlgorithm(algorithm, key.getBytes()); } diff --git a/samples/login-sample/gradle.properties b/samples/login-sample/gradle.properties index a1c9d5e1..aa248f2e 100644 --- a/samples/login-sample/gradle.properties +++ b/samples/login-sample/gradle.properties @@ -1,3 +1,3 @@ description=Login to Uber Sample -UBER_CLIENT_ID=insert_your_client_id_here -UBER_REDIRECT_URI=insert_your_redirect_uri_here \ No newline at end of file +#UBER_CLIENT_ID=insert_your_client_id_here +#UBER_REDIRECT_URI=insert_your_redirect_uri_here \ No newline at end of file diff --git a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java index d66c1144..1123de90 100644 --- a/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java +++ b/samples/login-sample/src/main/java/com/uber/sdk/android/samples/LoginSampleActivity.java @@ -104,15 +104,15 @@ protected void onCreate(Bundle savedInstanceState) { //Create a button with a custom request code whiteButton = (LoginButton) findViewById(R.id.uber_button_white); whiteButton.setCallback(new SampleLoginCallback()) - .setSessionConfiguration(configuration); + .setSessionConfiguration(configuration); //Create a button using a custom AccessTokenStorage //Custom Scopes are set using XML for this button as well in R.layout.activity_sample blackButton = (LoginButton) findViewById(R.id.uber_button_black); blackButton.setAccessTokenStorage(accessTokenStorage) - .setCallback(new SampleLoginCallback()) - .setSessionConfiguration(configuration) - .setRequestCode(LOGIN_BUTTON_CUSTOM_REQUEST_CODE); + .setCallback(new SampleLoginCallback()) + .setSessionConfiguration(configuration) + .setRequestCode(LOGIN_BUTTON_CUSTOM_REQUEST_CODE); //Use a custom button with an onClickListener to call the LoginManager directly @@ -184,21 +184,21 @@ private void loadProfileInfo() { service.getUserProfile() .enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - Toast.makeText(LoginSampleActivity.this, getString(R.string.greeting, response.body().getFirstName()), Toast.LENGTH_LONG).show(); - } else { - ApiError error = ErrorParser.parseError(response); - Toast.makeText(LoginSampleActivity.this, error.getClientErrors().get(0).getTitle(), Toast.LENGTH_LONG).show(); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - - } - }); + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(LoginSampleActivity.this, getString(R.string.greeting, response.body().getFirstName()), Toast.LENGTH_LONG).show(); + } else { + ApiError error = ErrorParser.parseError(response); + Toast.makeText(LoginSampleActivity.this, error.getClientErrors().get(0).getTitle(), Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + + } + }); } @Override diff --git a/samples/request-button-sample/src/main/AndroidManifest.xml b/samples/request-button-sample/src/main/AndroidManifest.xml index 6ea3b5bf..3bb1dfa4 100644 --- a/samples/request-button-sample/src/main/AndroidManifest.xml +++ b/samples/request-button-sample/src/main/AndroidManifest.xml @@ -38,13 +38,6 @@ - - - - - - From 0ad55cdc0e4803a41cd53536c0852f1ef71d4d7f Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Wed, 7 Feb 2018 18:35:25 -0800 Subject: [PATCH 17/19] Moving migration code to seperate class for better testing and easier removal in a number of months --- .../core/auth/LegacyUriRedirectHandler.java | 135 ++++++++++++++++ .../sdk/android/core/auth/LoginManager.java | 86 ++++------ .../uber/sdk/android/core/utils/Utility.java | 12 +- .../auth/LegacyUriRedirectHandlerTest.java | 121 ++++++++++++++ .../android/core/auth/LoginManagerTest.java | 151 ++++++++---------- samples/login-sample/gradle.properties | 4 +- 6 files changed, 357 insertions(+), 152 deletions(-) create mode 100644 core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java create mode 100644 core-android/src/test/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandlerTest.java diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java new file mode 100644 index 00000000..5bb4a9b0 --- /dev/null +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java @@ -0,0 +1,135 @@ +package com.uber.sdk.android.core.auth; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.v4.util.Pair; +import android.util.Log; + +import com.uber.sdk.android.core.UberSdk; +import com.uber.sdk.android.core.utils.Utility; +import com.uber.sdk.core.client.SessionConfiguration; + +class LegacyUriRedirectHandler { + + enum Mode { + OFF, + OLD_AUTH_CODE_FLOW, + MISSING_REDIRECT, + MISCONFIGURED_URI; + } + + private Mode mode = Mode.OFF; + + /** + * Will validate that the Redirect URI $FIELD_NAME_PREFIXMode is valid {@link Mode#OFF} and return true + * + * If false, then the app should terminate codeflow, this will happen in Debug mode for + * unhandled migration scenarios. + * See https://github.com/uber/rides-android-sdk#authentication-migration-version. + * + * @param activity to lookup package info and launch blocking dialog + * @param loginManager to validate old auth code flow + * @return true if valid, false if invalid + */ + boolean checkValidState(@NonNull Activity activity, @NonNull LoginManager + loginManager) { + initState(activity, loginManager); + + if (isLegacyMode()) { + final Pair titleAndMessage = getLegacyModeMessage(activity, loginManager); + final IllegalStateException exception = new IllegalStateException(titleAndMessage + .second); + Log.e(UberSdk.UBER_SDK_LOG_TAG, titleAndMessage.first, + exception); + + if(Utility.isDebugable(activity)) { + new AlertDialog.Builder(activity) + .setTitle(titleAndMessage.first) + .setMessage(titleAndMessage.second) + .setNeutralButton("Exit", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + throw exception; + } + }).show(); + return false; + } + } + return true; + } + + boolean isLegacyMode() { + return mode != Mode.OFF; + } + + private void initState(@NonNull Activity activity, @NonNull LoginManager loginManager) { + + SessionConfiguration sessionConfiguration = loginManager.getSessionConfiguration(); + boolean redirectForAuthorizationCode = loginManager.isRedirectForAuthorizationCode(); + + String generatedRedirectUri = activity.getPackageName().concat(".uberauth://redirect"); + String setRedirectUri = sessionConfiguration.getRedirectUri(); + + if (redirectForAuthorizationCode) { + mode = Mode.OLD_AUTH_CODE_FLOW; + } else if (sessionConfiguration.getRedirectUri() == null) { + mode = Mode.MISSING_REDIRECT; + } else if (!generatedRedirectUri.equals(setRedirectUri) && + !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri))) { + mode = Mode.MISCONFIGURED_URI; + } else { + mode = Mode.OFF; + } + + } + + + + private Pair getLegacyModeMessage(@NonNull Context context, @NonNull + LoginManager + loginManager) { + + final Pair titleAndMessage; + switch (mode) { + case OLD_AUTH_CODE_FLOW: + titleAndMessage = new Pair<>("Misconfigured SessionConfiguration, see log.", + "The Uber Authentication Flow for the Authorization Code Flow has " + + "been upgraded in 0.8.0 and a redirect URI must now be supplied to the application. " + + "You are seeing this error because the use of deprecated method " + + "LoginManager.setRedirectForAuthorizationCode() indicates your flow may not " + + "support the recent changes. See https://github" + + ".com/uber/rides-android-sdk#authentication-migration-version" + + "-08-and-above for resolution steps" + + "to insure your setup is correct and then migrate to the non-deprecate " + + "method LoginManager.setAuthCodeFlowEnabled()"); + break; + case MISSING_REDIRECT: + titleAndMessage = new Pair<>("Misconfigured SessionConfiguration, see log.", "Redirect URI must be set in " + + "Session Configuration."); + break; + case MISCONFIGURED_URI: + String generatedRedirectUri = context.getPackageName().concat("" + + ".uberauth://redirect"); + String setRedirectUri = loginManager.getSessionConfiguration().getRedirectUri(); + titleAndMessage = new Pair<>("Misconfigured redirect uri, see log.", + "Misconfigured redirect_uri. See https://github" + + ".com/uber/rides-android-sdk#authentication-migration-version-08-and-above" + + "for more info. Either 1) Register " + generatedRedirectUri + " as a " + + "redirect uri for the app at https://developer.uber.com/dashboard/ and " + + "specify this in your SessionConfiguration or 2) Override the default " + + "redirect_uri with the current one set (" + setRedirectUri + ") in the " + + "AndroidManifest."); + break; + default: + titleAndMessage = new Pair<>("Unknown URI Redirect Issue", "Unknown issue, see " + + "log"); + } + + return titleAndMessage; + + } +} diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java index c7bade13..3776daf4 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LoginManager.java @@ -24,7 +24,6 @@ import android.app.Activity; import android.content.Intent; -import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -33,7 +32,6 @@ import com.uber.sdk.android.core.UberSdk; import com.uber.sdk.android.core.install.SignupDeeplink; import com.uber.sdk.android.core.utils.AppProtocol; -import com.uber.sdk.android.core.utils.Utility; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.AccessTokenStorage; import com.uber.sdk.core.auth.Scope; @@ -42,7 +40,6 @@ import com.uber.sdk.core.client.Session; import com.uber.sdk.core.client.SessionConfiguration; -import static com.uber.sdk.android.core.utils.Utility.logOrError; import static com.uber.sdk.core.client.utils.Preconditions.checkNotEmpty; import static com.uber.sdk.core.client.utils.Preconditions.checkNotNull; @@ -97,12 +94,12 @@ public class LoginManager { private final LoginCallback callback; private final SessionConfiguration sessionConfiguration; private final int requestCode; + private final LegacyUriRedirectHandler legacyUriRedirectHandler; private boolean authCodeFlowEnabled = false; @Deprecated private boolean redirectForAuthorizationCode = false; - /** * @param accessTokenStorage to store access token. * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} @@ -137,10 +134,27 @@ public LoginManager( @NonNull LoginCallback loginCallback, @NonNull SessionConfiguration configuration, int requestCode) { + this(accessTokenStorage, loginCallback, configuration, requestCode, new LegacyUriRedirectHandler()); + } + + /** + * @param accessTokenStorage to store access token. + * @param loginCallback callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} is called. + * @param configuration to provide authentication information + * @param requestCode custom code to use for Activity communication + * @param legacyUriRedirectHandler Used to handle URI Redirect Migration + */ + LoginManager( + @NonNull AccessTokenStorage accessTokenStorage, + @NonNull LoginCallback loginCallback, + @NonNull SessionConfiguration configuration, + int requestCode, + @NonNull LegacyUriRedirectHandler legacyUriRedirectHandler) { this.accessTokenStorage = accessTokenStorage; this.callback = loginCallback; this.sessionConfiguration = configuration; this.requestCode = requestCode; + this.legacyUriRedirectHandler = legacyUriRedirectHandler; } /** @@ -148,12 +162,16 @@ public LoginManager( * * @param activity the activity used to start the {@link LoginActivity}. */ - public void login(@NonNull Activity activity) { + public void login(final @NonNull Activity activity) { checkNotEmpty(sessionConfiguration.getScopes(), "Scopes must be set in the Session " + "Configuration."); checkNotNull(sessionConfiguration.getRedirectUri(), "Redirect URI must be set in " + "Session Configuration."); + if (!legacyUriRedirectHandler.checkValidState(activity, this)) { + return; + } + SsoDeeplink ssoDeeplink = new SsoDeeplink.Builder(activity) .clientId(sessionConfiguration.getClientId()) .scopes(sessionConfiguration.getScopes()) @@ -178,11 +196,13 @@ public void login(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForImplicitGrant(@NonNull Activity activity) { - boolean forceWebView = - isInLegacyRedirectMode(activity); + + if (!legacyUriRedirectHandler.checkValidState(activity, this)) { + return; + } Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, - ResponseType.TOKEN, forceWebView); + ResponseType.TOKEN, legacyUriRedirectHandler.isLegacyMode()); activity.startActivityForResult(intent, requestCode); } @@ -192,10 +212,12 @@ public void loginForImplicitGrant(@NonNull Activity activity) { * @param activity to start Activity on. */ public void loginForAuthorizationCode(@NonNull Activity activity) { - boolean forceWebView = - isInLegacyRedirectMode(activity); + if (!legacyUriRedirectHandler.checkValidState(activity, this)) { + return; + } + Intent intent = LoginActivity.newIntent(activity, sessionConfiguration, - ResponseType.CODE, forceWebView); + ResponseType.CODE, legacyUriRedirectHandler.isLegacyMode()); activity.startActivityForResult(intent, requestCode); } @@ -411,46 +433,4 @@ private void handleResultOk(@Nullable Intent data) { } } - - boolean isInLegacyRedirectMode(@NonNull Activity activity) { - String generatedRedirectUri = activity.getPackageName().concat(".uberauth://redirect"); - String setRedirectUri = sessionConfiguration.getRedirectUri(); - - if (redirectForAuthorizationCode) { - String message = "The Uber Authentication Flow for the Authorization Code Flow has " - + "been upgraded in 0.8.0 and a redirect URI must now be supplied to the application. " - + "You are seeing this error because the use of deprecated method " - + "LoginManager.setRedirectForAuthorizationCode() indicates your flow may not " - + "support the recent changes. See https://github" - + ".com/uber/rides-android-sdk#authentication-migration-version" - + "-08-and-above for resolution steps" - + "to insure your setup is correct and then migrate to the non-deprecate " - + "method LoginManager.setAuthCodeFlowEnabled()"; - - logOrError(activity, new IllegalStateException(message)); - return true; - } - - if (sessionConfiguration.getRedirectUri() == null) { - String message = "Redirect URI must be set in " - + "Session Configuration."; - - logOrError(activity, new NullPointerException(message)); - return true; - } - - if (!generatedRedirectUri.equals(setRedirectUri) && - !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri))) { - String message = "Misconfigured redirect_uri. See https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above" - + "for more info. Either 1) Register " + generatedRedirectUri + " as a " - + "redirect uri for the app at https://developer.uber.com/dashboard/ and " - + "specify this in your SessionConfiguration or 2) Override the default " - + "redirect_uri with the current one set (" + setRedirectUri + ") in the " - + "AndroidManifest."; - logOrError(activity, new IllegalStateException(message)); - return true; - } - - return false; - } } diff --git a/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java b/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java index e4b606ed..fc364eea 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java @@ -1,10 +1,13 @@ package com.uber.sdk.android.core.utils; import android.app.Activity; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.support.annotation.NonNull; import android.util.Log; +import android.widget.Toast; import com.uber.sdk.android.core.UberSdk; @@ -32,15 +35,6 @@ public static boolean isDebugable(@NonNull Context context) { } - - public static void logOrError(Activity activity, Exception exception) { - if (Utility.isDebugable(activity)) { - throw new IllegalStateException(exception); - } else { - Log.e(UberSdk.UBER_SDK_LOG_TAG, exception.getMessage(), exception); - } - } - private static String hashWithAlgorithm(String algorithm, String key) { return hashWithAlgorithm(algorithm, key.getBytes()); } diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandlerTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandlerTest.java new file mode 100644 index 00000000..5005ac4a --- /dev/null +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandlerTest.java @@ -0,0 +1,121 @@ +package com.uber.sdk.android.core.auth; + +import android.app.Activity; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import com.uber.sdk.android.core.RobolectricTestBase; +import com.uber.sdk.core.client.SessionConfiguration; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class LegacyUriRedirectHandlerTest extends RobolectricTestBase { + + @Mock LoginManager loginManager; + @Mock PackageManager packageManager; + @Mock SessionConfiguration sessionConfiguration; + + Activity activity; + LegacyUriRedirectHandler legacyUriRedirectHandler; + + + private ApplicationInfo applicationInfo; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + applicationInfo = new ApplicationInfo(); + applicationInfo.flags = ApplicationInfo.FLAG_DEBUGGABLE; + activity = spy(Robolectric.setupActivity(Activity.class)); + //applicationInfo.flags = 0; + + when(sessionConfiguration.getRedirectUri()).thenReturn("com.example.uberauth://redirect"); + when(loginManager.getSessionConfiguration()).thenReturn(sessionConfiguration); + when(activity.getApplicationInfo()).thenReturn(applicationInfo); + when(activity.getPackageManager()).thenReturn(packageManager); + when(activity.getPackageName()).thenReturn("com.example"); + + legacyUriRedirectHandler = new LegacyUriRedirectHandler(); + } + + @Test + public void handleInvalidState_withMismatchingUriInDebug_invalidState() throws Exception { + when(sessionConfiguration.getRedirectUri()) + .thenReturn("com.example2.uberauth://redirect-uri"); + + assertThat(legacyUriRedirectHandler.checkValidState(activity, + loginManager)).isFalse(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + } + + @Test + public void handleInvalidState_withMismatchingUriInRelease_validState() throws Exception { + when(sessionConfiguration.getRedirectUri()) + .thenReturn("com.example2.uberauth://redirect-uri"); + applicationInfo.flags = 0; + + assertThat(legacyUriRedirectHandler.checkValidState(activity, + loginManager)).isTrue(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + } + + @Test + public void handleInvalidState_withLegacyAuthCodeFlowInDebug_invalidState() throws Exception { + when(loginManager.isRedirectForAuthorizationCode()).thenReturn(true); + + assertThat(legacyUriRedirectHandler.checkValidState(activity, loginManager)).isFalse(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + } + + @Test + public void handleInvalidState_withLegacyAuthCodeFlowInRelease_validState() throws Exception { + when(loginManager.isRedirectForAuthorizationCode()).thenReturn(true); + applicationInfo.flags = 0; + + assertThat(legacyUriRedirectHandler.checkValidState(activity, + loginManager)).isTrue(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + } + + @Test + public void handleInvalidState_withMissingRedirectUriInDebug_invalidState() throws Exception { + when(sessionConfiguration.getRedirectUri()) + .thenReturn(null); + + assertThat(legacyUriRedirectHandler.checkValidState(activity, + loginManager)).isFalse(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + } + + @Test + public void handleInvalidState_withMissingRedirectUriInRelease_validState() throws Exception { + when(sessionConfiguration.getRedirectUri()) + .thenReturn(null); + applicationInfo.flags = 0; + + assertThat(legacyUriRedirectHandler.checkValidState(activity, + loginManager)).isTrue(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + } + + @Test + public void handleInvalidState_withValidState_validState() throws Exception { + assertThat(legacyUriRedirectHandler.checkValidState(activity, + loginManager)).isTrue(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isFalse(); + } + + @Test + public void isLegacyMode_uninitialized_validState() throws Exception { + assertThat(legacyUriRedirectHandler.isLegacyMode()).isFalse(); + } +} \ No newline at end of file diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java index 0f2b767a..32ddb0a7 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LoginManagerTest.java @@ -63,6 +63,7 @@ import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -113,46 +114,51 @@ public class LoginManagerTest extends RobolectricTestBase { @Mock AccessTokenStorage accessTokenStorage; - SessionConfiguration SessionConfigurationWithSetUri; - SessionConfiguration SessionConfigurationWithGeneratedUri; + @Mock LegacyUriRedirectHandler legacyUriRedirectHandler; - private LoginManager loginManagerWithSetUri; - private LoginManager loginManagerwithGeneratedUri; + SessionConfiguration sessionConfiguration; - private ApplicationInfo debuggableApplicationInfo; - private ApplicationInfo releaseApplicationInfo; + private LoginManager loginManager; @Before public void setup() { - SessionConfigurationWithGeneratedUri = new SessionConfiguration.Builder().setClientId(CLIENT_ID) + sessionConfiguration = new SessionConfiguration.Builder().setClientId(CLIENT_ID) .setRedirectUri("com.example.uberauth://redirect") .setScopes(MIXED_SCOPES).build(); - loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, - SessionConfigurationWithGeneratedUri); + loginManager = new LoginManager(accessTokenStorage, callback, + sessionConfiguration, REQUEST_CODE_LOGIN_DEFAULT, + legacyUriRedirectHandler); - SessionConfigurationWithSetUri = new SessionConfiguration.Builder().setClientId(CLIENT_ID) - .setRedirectUri("com.custom://redirect") - .setScopes(MIXED_SCOPES).build(); - loginManagerWithSetUri = new LoginManager(accessTokenStorage, callback, - SessionConfigurationWithSetUri); when(activity.getPackageManager()).thenReturn(packageManager); when(activity.getApplicationInfo()).thenReturn(new ApplicationInfo()); when(activity.getPackageName()).thenReturn("com.example"); + when(legacyUriRedirectHandler.checkValidState(eq(activity), eq(loginManager))).thenReturn(true); + } + + @Test + public void login_withLegacyModeBlocking_shouldNotLogin() { + stubAppInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0], SsoDeeplink.MIN_VERSION_SUPPORTED); + when(legacyUriRedirectHandler.checkValidState(eq(activity), eq(loginManager))).thenReturn(false); + loginManager.login(activity); + + verify(activity, never()).startActivityForResult(any(Intent.class), anyInt()); + } - debuggableApplicationInfo = new ApplicationInfo(); - debuggableApplicationInfo.flags = ApplicationInfo.FLAG_DEBUGGABLE; - releaseApplicationInfo = new ApplicationInfo(); - releaseApplicationInfo.flags = 0; - when(activity.getApplicationInfo()).thenReturn(debuggableApplicationInfo); + @Test + public void login_withLegacyModeNotBlocking_shouldLogin() { + stubAppInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0], SsoDeeplink.MIN_VERSION_SUPPORTED); + when(legacyUriRedirectHandler.checkValidState(eq(activity), eq(loginManager))).thenReturn(true); + loginManager.login(activity); + verify(activity).startActivityForResult(any(Intent.class), anyInt()); } @Test public void loginWithAppInstalledPrivilegedScopes_shouldLaunchIntent() { stubAppInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0], SsoDeeplink.MIN_VERSION_SUPPORTED); - loginManagerwithGeneratedUri.login(activity); + loginManager.login(activity); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(Integer.class); @@ -165,12 +171,12 @@ public void loginWithAppInstalledPrivilegedScopes_shouldLaunchIntent() { @Test public void loginWithAppInstalledPrivilegedScopesAndRequestCode_shouldLaunchIntent() { - loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, - SessionConfigurationWithGeneratedUri, REQUEST_CODE); + loginManager = new LoginManager(accessTokenStorage, callback, + sessionConfiguration, REQUEST_CODE); stubAppInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0], SsoDeeplink.MIN_VERSION_SUPPORTED); - loginManagerwithGeneratedUri.login(activity); + loginManager.login(activity); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(Integer.class); @@ -183,13 +189,13 @@ public void loginWithAppInstalledPrivilegedScopesAndRequestCode_shouldLaunchInte @Test public void loginWithoutAppInstalledGeneralScopes_shouldLaunchWebView() { - SessionConfigurationWithGeneratedUri = SessionConfigurationWithGeneratedUri.newBuilder().setScopes(GENERAL_SCOPES).build(); - loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, - SessionConfigurationWithGeneratedUri); + sessionConfiguration = sessionConfiguration.newBuilder().setScopes(GENERAL_SCOPES).build(); + loginManager = new LoginManager(accessTokenStorage, callback, + sessionConfiguration); stubAppNotInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0]); - loginManagerwithGeneratedUri.login(activity); + loginManager.login(activity); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(Integer.class); @@ -208,14 +214,14 @@ public void loginWithoutAppInstalledPrivilegedScopes_shouldLaunchAppInstall() { final Activity activity = spy(Robolectric.setupActivity(Activity.class)); when(activity.getPackageManager()).thenReturn(packageManager); - SessionConfigurationWithGeneratedUri = SessionConfigurationWithGeneratedUri.newBuilder().build(); - loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, - SessionConfigurationWithGeneratedUri) + sessionConfiguration = sessionConfiguration.newBuilder().build(); + loginManager = new LoginManager(accessTokenStorage, callback, + sessionConfiguration) .setAuthCodeFlowEnabled(false); stubAppNotInstalled(packageManager, AppProtocol.UBER_PACKAGE_NAMES[0]); - loginManagerwithGeneratedUri.login(activity); + loginManager.login(activity); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -233,7 +239,7 @@ public void onActivityResult_whenResultOkAndHasToken_shouldCallbackSuccess() { .putExtra(EXTRA_EXPIRES_IN, ACCESS_TOKEN.getExpiresIn()) .putExtra(EXTRA_TOKEN_TYPE, ACCESS_TOKEN.getTokenType()); - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, intent); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, intent); ArgumentCaptor storedToken = ArgumentCaptor.forClass(AccessToken.class); ArgumentCaptor returnedToken = ArgumentCaptor.forClass(AccessToken.class); @@ -249,7 +255,7 @@ public void onActivityResult_whenResultOkAndHasCode_shouldCallbackSuccess() { Intent intent = new Intent() .putExtra(EXTRA_CODE_RECEIVED, AUTHORIZATION_CODE); - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, intent); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, intent); ArgumentCaptor capturedCode = ArgumentCaptor.forClass(String.class); verify(callback).onAuthorizationCodeReceived(capturedCode.capture()); @@ -259,7 +265,7 @@ public void onActivityResult_whenResultOkAndHasCode_shouldCallbackSuccess() { @Test public void onActivityResult_whenResultCanceledAndNoData_shouldCallbackCancel() { - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); verify(callback).onLoginCancel(); } @@ -268,26 +274,26 @@ public void onActivityResult_whenResultCanceledAndHasData_shouldCallbackError() Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.INVALID_RESPONSE .toStandardString()); - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); verify(callback).onLoginError(AuthenticationError.INVALID_RESPONSE); } @Test public void onActivityResult_whenResultCanceledAndNoData_shouldCancel() { - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, null); verify(callback).onLoginCancel(); } @Test public void onActivityResult_whenResultOkAndNoData_shouldCallbackErrorUnknown() { - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, null); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_OK, null); verify(callback).onLoginError(AuthenticationError.UNKNOWN); } @Test public void onActivityResult_whenRequestCodeDoesNotMatch_nothingShouldHappen() { Intent intent = mock(Intent.class); - loginManagerwithGeneratedUri.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); + loginManager.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); verifyZeroInteractions(intent); verifyZeroInteractions(callback); } @@ -295,7 +301,7 @@ public void onActivityResult_whenRequestCodeDoesNotMatch_nothingShouldHappen() { @Test public void onActivityResult_whenResultCanceledAndDataButNoCallback_nothingShouldHappen() { Intent intent = mock(Intent.class); - loginManagerwithGeneratedUri.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); + loginManager.onActivityResult(activity, 1337, Activity.RESULT_OK, intent); verifyZeroInteractions(intent); } @@ -303,8 +309,8 @@ public void onActivityResult_whenResultCanceledAndDataButNoCallback_nothingShoul public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerAuthorizationCode() { Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.UNAVAILABLE.toStandardString()); - loginManagerwithGeneratedUri.setAuthCodeFlowEnabled(true); - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); + loginManager.setAuthCodeFlowEnabled(true); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -323,25 +329,25 @@ public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerAut @Test public void onActivityResult_whenUnavailableAndPrivilegedScopesNoRedirect_shouldError() { Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.UNAVAILABLE.toStandardString()); - SessionConfigurationWithGeneratedUri = SessionConfigurationWithGeneratedUri.newBuilder().build(); - loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, - SessionConfigurationWithGeneratedUri) + sessionConfiguration = sessionConfiguration.newBuilder().build(); + loginManager = new LoginManager(accessTokenStorage, callback, + sessionConfiguration) .setAuthCodeFlowEnabled(false); - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); verify(callback).onLoginError(AuthenticationError.UNAVAILABLE); } @Test public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerImplicitGrant() { - SessionConfigurationWithGeneratedUri = SessionConfigurationWithGeneratedUri.newBuilder().setScopes(GENERAL_SCOPES).build(); - loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, - SessionConfigurationWithGeneratedUri); + sessionConfiguration = sessionConfiguration.newBuilder().setScopes(GENERAL_SCOPES).build(); + loginManager = new LoginManager(accessTokenStorage, callback, + sessionConfiguration); Intent intent = new Intent().putExtra(EXTRA_ERROR, AuthenticationError.UNAVAILABLE.toStandardString()); - loginManagerwithGeneratedUri.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); + loginManager.onActivityResult(activity, REQUEST_CODE_LOGIN_DEFAULT, Activity.RESULT_CANCELED, intent); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -360,74 +366,43 @@ public void onActivityResult_whenUnavailableAndPrivilegedScopes_shouldTriggerImp @Test public void isAuthenticated_withServerToken_true() { when(accessTokenStorage.getAccessToken()).thenReturn(null); - loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, SessionConfigurationWithGeneratedUri + loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration .newBuilder().setServerToken("serverToken").build()); - assertTrue(loginManagerwithGeneratedUri.isAuthenticated()); + assertTrue(loginManager.isAuthenticated()); } @Test public void isAuthenticated_withAccessToken_true() { when(accessTokenStorage.getAccessToken()).thenReturn(ACCESS_TOKEN); - assertTrue(loginManagerwithGeneratedUri.isAuthenticated()); + assertTrue(loginManager.isAuthenticated()); } @Test public void isAuthenticated_withoutAccessOrServerToken_false() { when(accessTokenStorage.getAccessToken()).thenReturn(null); - assertFalse(loginManagerwithGeneratedUri.isAuthenticated()); + assertFalse(loginManager.isAuthenticated()); } @Test public void getSession_withServerToken_successful() { when(accessTokenStorage.getAccessToken()).thenReturn(null); - loginManagerwithGeneratedUri = new LoginManager(accessTokenStorage, callback, SessionConfigurationWithGeneratedUri + loginManager = new LoginManager(accessTokenStorage, callback, sessionConfiguration .newBuilder().setServerToken("serverToken").build()); - Session session = loginManagerwithGeneratedUri.getSession(); + Session session = loginManager.getSession(); assertEquals("serverToken", session.getAuthenticator().getSessionConfiguration().getServerToken()); } @Test public void getSession_withAccessToken_successful() { when(accessTokenStorage.getAccessToken()).thenReturn(ACCESS_TOKEN); - Session session = loginManagerwithGeneratedUri.getSession(); + Session session = loginManager.getSession(); assertEquals(ACCESS_TOKEN, ((AccessTokenAuthenticator)session.getAuthenticator()).getTokenStorage().getAccessToken()); } @Test(expected = IllegalStateException.class) public void getSession_withoutAccessTokenOrToken_fails() { when(accessTokenStorage.getAccessToken()).thenReturn(null); - loginManagerwithGeneratedUri.getSession(); - } - - @Test(expected = IllegalStateException.class) - public void isInLegacyRedirectMode_whenMismatchingUriInDebug_throwsException() { - loginManagerWithSetUri.isInLegacyRedirectMode(activity); - } - - @Test() - public void isInLegacyRedirectMode_whenMismatchingUriInRelease_logsErrorAndReturnsTrue() { - when(activity.getApplicationInfo()).thenReturn(releaseApplicationInfo); - assertThat(loginManagerWithSetUri.isInLegacyRedirectMode(activity)).isTrue(); - } - - - @Test(expected = IllegalStateException.class) - public void isInLegacyRedirectMode_whenLegacyAuthCodeFlowInDebug_throwsException() { - loginManagerwithGeneratedUri.setRedirectForAuthorizationCode(true); - loginManagerwithGeneratedUri.isInLegacyRedirectMode(activity); - - } - - @Test() - public void isInLegacyRedirectMode_whenLegacyAuthCodeFlowInRelease_logsErrorAndReturnsTrue() { - when(activity.getApplicationInfo()).thenReturn(releaseApplicationInfo); - loginManagerwithGeneratedUri.setRedirectForAuthorizationCode(true); - assertThat(loginManagerwithGeneratedUri.isInLegacyRedirectMode(activity)).isTrue(); - } - - @Test() - public void isInLegacyRedirectMode_whenMatchingUri_returnsFalse() { - assertThat(loginManagerwithGeneratedUri.isInLegacyRedirectMode(activity)).isFalse(); + loginManager.getSession(); } private static PackageManager stubAppInstalled(PackageManager packageManager, String packageName, int versionCode) { diff --git a/samples/login-sample/gradle.properties b/samples/login-sample/gradle.properties index aa248f2e..a1c9d5e1 100644 --- a/samples/login-sample/gradle.properties +++ b/samples/login-sample/gradle.properties @@ -1,3 +1,3 @@ description=Login to Uber Sample -#UBER_CLIENT_ID=insert_your_client_id_here -#UBER_REDIRECT_URI=insert_your_redirect_uri_here \ No newline at end of file +UBER_CLIENT_ID=insert_your_client_id_here +UBER_REDIRECT_URI=insert_your_redirect_uri_here \ No newline at end of file From 01bf6740c1c6723db7fbb4a8c745bba4f5b9b552 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Wed, 7 Feb 2018 18:37:30 -0800 Subject: [PATCH 18/19] Fix typo --- .../sdk/android/core/auth/LegacyUriRedirectHandler.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java index 5bb4a9b0..af9401f6 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java @@ -13,6 +13,12 @@ import com.uber.sdk.android.core.utils.Utility; import com.uber.sdk.core.client.SessionConfiguration; +/** + * Manages migration problems from old style of redirect URI handling to newer version for Custom + * tabs support. + * + * See https://github.com/uber/rides-android-sdk#authentication-migration-version. + */ class LegacyUriRedirectHandler { enum Mode { @@ -25,7 +31,7 @@ enum Mode { private Mode mode = Mode.OFF; /** - * Will validate that the Redirect URI $FIELD_NAME_PREFIXMode is valid {@link Mode#OFF} and return true + * Will validate that the Redirect URI mode is valid {@link Mode#OFF} and return true * * If false, then the app should terminate codeflow, this will happen in Debug mode for * unhandled migration scenarios. From 340dca0e46c511759de176c6b4d297695e36b9ef Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Thu, 8 Feb 2018 17:48:09 -0800 Subject: [PATCH 19/19] Fixed auth code flow bug, added more tests, better docs --- README.md | 40 +++++- .../core/auth/LegacyUriRedirectHandler.java | 68 ++++----- .../uber/sdk/android/core/utils/Utility.java | 32 ++++- .../main/res/values/strings_unlocalized.xml | 26 ++++ .../auth/LegacyUriRedirectHandlerTest.java | 130 ++++++++++++++++-- 5 files changed, 248 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index e0604605..3fe659eb 100644 --- a/README.md +++ b/README.md @@ -216,10 +216,14 @@ With Version 0.8 and above of the SDK, the redirect URI is more strongly enforce standards [IETF RFC](https://tools.ietf.org/html/draft-ietf-oauth-native-apps-12). The SDK will automatically created a redirect URI to be used in the oauth callbacks with -the format "applicationId.uberauth", ex "com.example.uberauth". If this differs from the previous -specified redirect URI configured in the SessionConfiguration, there are two options. +the format "applicationId.uberauth", ex "com.example.uberauth". **This URI must be registered in +the [developer dashboard](https://developer.uber.com/dashboard)** - 1. Change the redirect URI to match the new scheme in the configuration of the Session. If this is left out entirely, the default will be used. +If this differs from the previous specified redirect URI configured in the SessionConfiguration, +there are a few options. + + 1. Change the redirect URI to match the new scheme in the configuration of the Session. If this + is left out entirely, the default will be used. ```java SessionConfiguration config = new SessionConfiguration.Builder() @@ -228,7 +232,7 @@ SessionConfiguration config = new SessionConfiguration.Builder() ``` 2. Override the LoginRedirectReceiverActivity in your main manifest and provide a custom intent -filter. +filter. Register this custom URI in the developer dashboard for your application. ```xml - + ``` +3. If using [Authorization Code Flow](https://developer.uber.com/docs/riders/guides/authentication/user-access-token), you will need to configure your server to redirect to + the Mobile Application with an access token either via the generated URI or a custom URI as defined in steps 1 and 2. + +The Session should be configured to redirect to the server to do a code exchange and the login +manager should indicate the SDK is operating in the Authorization Code Flow. + +```java +SessionConfiguration config = new SessionConfiguration.Builder() + .setRedirectUri("example.com/redirect") //Where this is your configured server + .build(); + +loginManager.setAuthCodeEnabled(true); +loginManager.login(this); + +``` + + Once the code is exchanged, the server should redirect to a URI in the standard OAUTH format of + `com.example.uberauth://redirect#access_token=ACCESS_TOKEN&token_type=Bearer&expires_in=TTL&scope=SCOPES` + for the SDK to receive the access token and continue operation.`` + + +##### Authorization Code Flow + + The default behavior of calling `LoginManager.login(activity)` is to activate Single Sign On, and if SSO is unavailable, fallback to Implicit Grant if privileged scopes are not requested, otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager .setAuthCodeEnabled(true)` to prevent the redirect to the Play Store. Implicit Grant will allow access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes). + #### Login Errors Upon a failure to login, an `AuthenticationError` will be provided in the `LoginCallback`. This enum provides a series of values that provide more information on the type of error. diff --git a/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java b/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java index af9401f6..8ec61f17 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandler.java @@ -1,15 +1,12 @@ package com.uber.sdk.android.core.auth; import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.net.Uri; import android.support.annotation.NonNull; import android.support.v4.util.Pair; -import android.util.Log; -import com.uber.sdk.android.core.UberSdk; +import com.uber.sdk.android.core.R; import com.uber.sdk.android.core.utils.Utility; import com.uber.sdk.core.client.SessionConfiguration; @@ -23,9 +20,9 @@ class LegacyUriRedirectHandler { enum Mode { OFF, - OLD_AUTH_CODE_FLOW, + MISCONFIGURED_AUTH_CODE_FLOW, MISSING_REDIRECT, - MISCONFIGURED_URI; + MISMATCHING_URI; } private Mode mode = Mode.OFF; @@ -44,28 +41,16 @@ enum Mode { boolean checkValidState(@NonNull Activity activity, @NonNull LoginManager loginManager) { initState(activity, loginManager); - + boolean validToContinueExecution = true; if (isLegacyMode()) { - final Pair titleAndMessage = getLegacyModeMessage(activity, loginManager); - final IllegalStateException exception = new IllegalStateException(titleAndMessage - .second); - Log.e(UberSdk.UBER_SDK_LOG_TAG, titleAndMessage.first, - exception); - - if(Utility.isDebugable(activity)) { - new AlertDialog.Builder(activity) - .setTitle(titleAndMessage.first) - .setMessage(titleAndMessage.second) - .setNeutralButton("Exit", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - throw exception; - } - }).show(); - return false; - } + String logMessage = activity.getString(getLegacyModeErrorMessage()); + String uiTitle = activity.getString(R.string.ub__misconfigured_redirect_uri_title); + String uiMessage = activity.getString(R.string.ub__misconfigured_redirect_uri_message); + + validToContinueExecution = !Utility.logAndShowBlockingDebugUIAlert(activity, + logMessage, uiTitle, uiMessage, new IllegalStateException(logMessage)); } - return true; + return validToContinueExecution; } boolean isLegacyMode() { @@ -81,12 +66,13 @@ private void initState(@NonNull Activity activity, @NonNull LoginManager loginMa String setRedirectUri = sessionConfiguration.getRedirectUri(); if (redirectForAuthorizationCode) { - mode = Mode.OLD_AUTH_CODE_FLOW; + mode = Mode.MISCONFIGURED_AUTH_CODE_FLOW; } else if (sessionConfiguration.getRedirectUri() == null) { mode = Mode.MISSING_REDIRECT; } else if (!generatedRedirectUri.equals(setRedirectUri) && - !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri))) { - mode = Mode.MISCONFIGURED_URI; + !AuthUtils.isRedirectUriRegistered(activity, Uri.parse(setRedirectUri)) && + !loginManager.isAuthCodeFlowEnabled()) { + mode = Mode.MISMATCHING_URI; } else { mode = Mode.OFF; } @@ -101,8 +87,8 @@ private Pair getLegacyModeMessage(@NonNull Context context, @Non final Pair titleAndMessage; switch (mode) { - case OLD_AUTH_CODE_FLOW: - titleAndMessage = new Pair<>("Misconfigured SessionConfiguration, see log.", + case MISCONFIGURED_AUTH_CODE_FLOW: + titleAndMessage = new Pair<>("Misconfigured Redirect URI - See log.", "The Uber Authentication Flow for the Authorization Code Flow has " + "been upgraded in 0.8.0 and a redirect URI must now be supplied to the application. " + "You are seeing this error because the use of deprecated method " @@ -114,14 +100,15 @@ private Pair getLegacyModeMessage(@NonNull Context context, @Non + "method LoginManager.setAuthCodeFlowEnabled()"); break; case MISSING_REDIRECT: - titleAndMessage = new Pair<>("Misconfigured SessionConfiguration, see log.", "Redirect URI must be set in " + titleAndMessage = new Pair<>("Null Redirect URI - See log.", + "Redirect URI must be set in " + "Session Configuration."); break; - case MISCONFIGURED_URI: + case MISMATCHING_URI: String generatedRedirectUri = context.getPackageName().concat("" + ".uberauth://redirect"); String setRedirectUri = loginManager.getSessionConfiguration().getRedirectUri(); - titleAndMessage = new Pair<>("Misconfigured redirect uri, see log.", + titleAndMessage = new Pair<>("Misconfigured Redirect URI - See log.", "Misconfigured redirect_uri. See https://github" + ".com/uber/rides-android-sdk#authentication-migration-version-08-and-above" + "for more info. Either 1) Register " + generatedRedirectUri + " as a " @@ -138,4 +125,17 @@ private Pair getLegacyModeMessage(@NonNull Context context, @Non return titleAndMessage; } + + private int getLegacyModeErrorMessage() { + switch (mode) { + case MISCONFIGURED_AUTH_CODE_FLOW: + return R.string.ub__misconfigured_auth_code_flow_log; + case MISSING_REDIRECT: + return R.string.ub__missing_redirect_uri_log; + case MISMATCHING_URI: + return R.string.ub__mismatching_redirect_uri_log; + default: + return 0; + } + } } diff --git a/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java b/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java index fc364eea..6ccade0f 100644 --- a/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java +++ b/core-android/src/main/java/com/uber/sdk/android/core/utils/Utility.java @@ -7,8 +7,8 @@ import android.content.pm.ApplicationInfo; import android.support.annotation.NonNull; import android.util.Log; -import android.widget.Toast; +import com.uber.sdk.android.core.R; import com.uber.sdk.android.core.UberSdk; import java.security.MessageDigest; @@ -35,6 +35,36 @@ public static boolean isDebugable(@NonNull Context context) { } + /** + * Logs error and when debug is enabled, shows Alert Dialog with debug instructions. + * + * @param activity + * @param logMessage + * @param alertTitle + * @param alertMessage + * @return true if developer error is shown, false otherwise. + */ + public static boolean logAndShowBlockingDebugUIAlert(@NonNull Activity activity, + final @NonNull String logMessage, + final @NonNull String alertTitle, + final @NonNull String alertMessage, + final @NonNull RuntimeException exception) { + Log.e(UberSdk.UBER_SDK_LOG_TAG, logMessage, exception); + + if(Utility.isDebugable(activity)) { + new AlertDialog.Builder(activity) + .setTitle(alertTitle) + .setMessage(alertMessage) + .setNeutralButton(R.string.ub__alert_dialog_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }).show(); + return true; + } + return false; + } + private static String hashWithAlgorithm(String algorithm, String key) { return hashWithAlgorithm(algorithm, key.getBytes()); } diff --git a/core-android/src/main/res/values/strings_unlocalized.xml b/core-android/src/main/res/values/strings_unlocalized.xml index 7a7b0b7e..7924bce2 100644 --- a/core-android/src/main/res/values/strings_unlocalized.xml +++ b/core-android/src/main/res/values/strings_unlocalized.xml @@ -25,4 +25,30 @@ xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"> https://m.uber.com/sign-up?client_id=%1$s&user-agent=%2$s + + OK + Misconfigured Redirect URI + + An invalid use of the redirect URI for + authentication has been detected from an older version of the Uber SDK. + \n\n + Read https://github.com/uber/rides-android-sdk#authentication-migration-version for migration + information. See logcat for more details. + + + + LoginManager.setRedirectForAuthorizationCode() is deprecated in versions > 0.8.0. + \n\n + See https://github.com/uber/rides-android-sdk#authentication-migration-version for + information on using LoginManager.setAuthCodeFlowEnabled() with a properly registered URI + for com.uber.sdk.android.core.auth.LoginRedirectReceiverActivity in the AndroidManifest.xml. + + Redirect URI must be set in Session Configuration. + + Redirect URI set in SessionConfiguration does not match URI registered in + AndroidManifest.xml for com.uber.sdk.android.core.auth.LoginRedirectReceiverActivity. + \n\n + See https://github.com/uber/rides-android-sdk#authentication-migration-version for + configuration required in versions > 0.8.0. + diff --git a/core-android/src/test/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandlerTest.java b/core-android/src/test/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandlerTest.java index 5005ac4a..38a77e12 100644 --- a/core-android/src/test/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandlerTest.java +++ b/core-android/src/test/java/com/uber/sdk/android/core/auth/LegacyUriRedirectHandlerTest.java @@ -1,21 +1,36 @@ package com.uber.sdk.android.core.auth; import android.app.Activity; +import android.app.AlertDialog; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import com.uber.sdk.android.core.BuildConfig; +import com.uber.sdk.android.core.R; import com.uber.sdk.android.core.RobolectricTestBase; +import com.uber.sdk.android.core.UberSdk; import com.uber.sdk.core.client.SessionConfiguration; 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.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowAlertDialog; +import org.robolectric.shadows.ShadowLog; + +import java.util.List; +import java.util.logging.LogManager; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; public class LegacyUriRedirectHandlerTest extends RobolectricTestBase { @@ -26,6 +41,13 @@ public class LegacyUriRedirectHandlerTest extends RobolectricTestBase { Activity activity; LegacyUriRedirectHandler legacyUriRedirectHandler; + String misconfiguredAuthCode; + String missingRedirectUri; + String mismatchingRedirectUri; + + String alertTitle; + String alertMessage; + private ApplicationInfo applicationInfo; @@ -45,20 +67,36 @@ public void setup() { when(activity.getPackageName()).thenReturn("com.example"); legacyUriRedirectHandler = new LegacyUriRedirectHandler(); + + misconfiguredAuthCode = RuntimeEnvironment.application.getString( + R.string.ub__misconfigured_auth_code_flow_log); + missingRedirectUri = RuntimeEnvironment.application.getString( + R.string.ub__missing_redirect_uri_log); + mismatchingRedirectUri = RuntimeEnvironment.application.getString( + R.string.ub__mismatching_redirect_uri_log); + + alertTitle = RuntimeEnvironment.application.getString( + R.string.ub__misconfigured_redirect_uri_title); + alertMessage = RuntimeEnvironment.application.getString( + R.string.ub__misconfigured_redirect_uri_message); } @Test - public void handleInvalidState_withMismatchingUriInDebug_invalidState() throws Exception { + public void handleInvalidState_withMismatchingUriInDebug_invalidStateWithAlertDialog() { when(sessionConfiguration.getRedirectUri()) .thenReturn("com.example2.uberauth://redirect-uri"); assertThat(legacyUriRedirectHandler.checkValidState(activity, loginManager)).isFalse(); assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + + assertDialogShown(); + assertLastLog(mismatchingRedirectUri); + } @Test - public void handleInvalidState_withMismatchingUriInRelease_validState() throws Exception { + public void handleInvalidState_withMismatchingUriInRelease_validStateWithLog() { when(sessionConfiguration.getRedirectUri()) .thenReturn("com.example2.uberauth://redirect-uri"); applicationInfo.flags = 0; @@ -66,38 +104,50 @@ public void handleInvalidState_withMismatchingUriInRelease_validState() throws E assertThat(legacyUriRedirectHandler.checkValidState(activity, loginManager)).isTrue(); assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + + assertNoDialogShown(); + assertLastLog(mismatchingRedirectUri); } @Test - public void handleInvalidState_withLegacyAuthCodeFlowInDebug_invalidState() throws Exception { + public void handleInvalidState_withLegacyAuthCodeFlowInDebug_invalidStatWithAlertDialog() { when(loginManager.isRedirectForAuthorizationCode()).thenReturn(true); assertThat(legacyUriRedirectHandler.checkValidState(activity, loginManager)).isFalse(); assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + + assertDialogShown(); + assertLastLog(misconfiguredAuthCode); } @Test - public void handleInvalidState_withLegacyAuthCodeFlowInRelease_validState() throws Exception { + public void handleInvalidState_withLegacyAuthCodeFlowInRelease_validState() { when(loginManager.isRedirectForAuthorizationCode()).thenReturn(true); applicationInfo.flags = 0; assertThat(legacyUriRedirectHandler.checkValidState(activity, loginManager)).isTrue(); assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + + assertNoDialogShown(); + assertLastLog(misconfiguredAuthCode); } @Test - public void handleInvalidState_withMissingRedirectUriInDebug_invalidState() throws Exception { + public void handleInvalidState_withMissingRedirectUriInDebug_invalidStateWithAlertDialog() { when(sessionConfiguration.getRedirectUri()) .thenReturn(null); assertThat(legacyUriRedirectHandler.checkValidState(activity, loginManager)).isFalse(); assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + + assertDialogShown(); + assertLastLog(missingRedirectUri); } @Test - public void handleInvalidState_withMissingRedirectUriInRelease_validState() throws Exception { + public void handleInvalidState_withMissingRedirectUriInRelease_validState() { when(sessionConfiguration.getRedirectUri()) .thenReturn(null); applicationInfo.flags = 0; @@ -105,17 +155,81 @@ public void handleInvalidState_withMissingRedirectUriInRelease_validState() thro assertThat(legacyUriRedirectHandler.checkValidState(activity, loginManager)).isTrue(); assertThat(legacyUriRedirectHandler.isLegacyMode()).isTrue(); + + assertNoDialogShown(); + assertLastLog(missingRedirectUri); + } + + @Test + public void handleInvalidState_withMatchingRedirectUriAndNoLegacyAuthCodeFlow_validState() { + assertThat(legacyUriRedirectHandler.checkValidState(activity, + loginManager)).isTrue(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isFalse(); + + assertNoDialogShown(); + assertNoLogs(); + } + + @Test + public void handleInvalidState_withAuthCodeFlowAndMisMatchingRedirectUriInDebug_validState() { + when(sessionConfiguration.getRedirectUri()) + .thenReturn("com.example2.uberauth://redirect-uri"); + when(loginManager.isAuthCodeFlowEnabled()).thenReturn(true); + + assertThat(legacyUriRedirectHandler.checkValidState(activity, + loginManager)).isTrue(); + assertThat(legacyUriRedirectHandler.isLegacyMode()).isFalse(); + + assertNoDialogShown(); + assertNoLogs(); } @Test - public void handleInvalidState_withValidState_validState() throws Exception { + public void handleInvalidState_withAuthCodeFlowAndMisMatchingRedirectUriInRelease_validState() { + when(sessionConfiguration.getRedirectUri()) + .thenReturn("com.example2.uberauth://redirect-uri"); + when(loginManager.isAuthCodeFlowEnabled()).thenReturn(true); + applicationInfo.flags = 0; + assertThat(legacyUriRedirectHandler.checkValidState(activity, loginManager)).isTrue(); assertThat(legacyUriRedirectHandler.isLegacyMode()).isFalse(); + + assertNoDialogShown(); + assertNoLogs(); } @Test - public void isLegacyMode_uninitialized_validState() throws Exception { + public void isLegacyMode_uninitialized_validState() { assertThat(legacyUriRedirectHandler.isLegacyMode()).isFalse(); + + assertNoDialogShown(); + assertNoLogs(); + } + + private void assertLastLog(String message) { + List logItemList = ShadowLog.getLogsForTag(UberSdk.UBER_SDK_LOG_TAG); + assertThat(ShadowLog.getLogsForTag(UberSdk.UBER_SDK_LOG_TAG)).isNotEmpty(); + ShadowLog.LogItem logItem = logItemList.get(logItemList.size()-1); + assertThat(logItem.msg).isEqualTo(message); + } + + private void assertNoLogs() { + List logItemList = ShadowLog.getLogsForTag(UberSdk.UBER_SDK_LOG_TAG); + assertThat(ShadowLog.getLogsForTag(UberSdk.UBER_SDK_LOG_TAG)).isNull(); + } + + private void assertDialogShown() { + AlertDialog alertDialog = ShadowAlertDialog.getLatestAlertDialog(); + assertThat(alertDialog.isShowing()).isTrue(); + assertThat(shadowOf(alertDialog).getTitle()) + .isEqualTo(alertTitle); + assertThat(shadowOf(alertDialog).getMessage()) + .isEqualTo(alertMessage); + } + + private void assertNoDialogShown() { + AlertDialog alertDialog = ShadowAlertDialog.getLatestAlertDialog(); + assertThat(alertDialog).isNull(); } } \ No newline at end of file