Index: net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java |
diff --git a/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java b/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java |
index bbc0583d84685c3802eac52ba880fc12649e969b..a965196633f169c1e4b001e07b9484400a2c2061 100644 |
--- a/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java |
+++ b/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java |
@@ -4,6 +4,7 @@ |
package org.chromium.net; |
+import android.Manifest; |
import android.accounts.Account; |
import android.accounts.AccountManager; |
import android.accounts.AccountManagerCallback; |
@@ -16,6 +17,7 @@ |
import android.content.Intent; |
import android.content.IntentFilter; |
import android.content.pm.PackageManager; |
+import android.os.Build; |
import android.os.Bundle; |
import android.os.Handler; |
import android.os.Process; |
@@ -33,6 +35,26 @@ |
/** |
* Class to get Auth Tokens for HTTP Negotiate authentication (typically used for Kerberos) An |
* instance of this class is created for each separate negotiation. |
+ * |
+ * Please keep the documentation from the chromium.org page (https://goo.gl/46hmKx) in sync. |
+ * ================================================================================================ |
+ * In addition to the error codes that can be forwarded from the authenticator app, the following |
+ * errors can be displayed when trying to authenticate a request: |
+ * |
+ * - ERR_UNEXPECTED: An unexpected error happened and the request has been terminated. |
+ * |
+ * - ERR_MISSING_AUTH_CREDENTIALS: The account information is not usable. It can be raised for |
+ * example if the user did not log in to the authenticator app and no eligible account is found, |
+ * if the account information can't be obtained because the current app does not have the |
+ * required permissions, or if there is more than one eligible account and we can't obtain a |
+ * selection from the user. |
+ * |
+ * - ERR_MISCONFIGURED_AUTH_ENVIRONMENT: The authentication can't be completed because of some |
+ * issues in the configuration of the app. Some permissions may be missing. |
+ * |
+ * Please search for the "cr_net_auth" tag in logcat to have more information about the cause of |
+ * these errors. |
+ * ================================================================================================ |
*/ |
@JNINamespace("net::android") |
public class HttpNegotiateAuthenticator { |
@@ -78,22 +100,37 @@ public void run(AccountManagerFuture<Account[]> future) { |
try { |
accounts = future.getResult(); |
} catch (OperationCanceledException | AuthenticatorException | IOException e) { |
- Log.w(TAG, "Unable to retrieve the results for the getAccounts call.", e); |
+ Log.w(TAG, "ERR_UNEXPECTED: Error while attempting to retrieve accounts.", e); |
nativeSetResult(mRequestData.nativeResultObject, NetError.ERR_UNEXPECTED, null); |
return; |
} |
- if (accounts.length != 1) { |
- Log.w(TAG, "Unable to obtain a unique eligible account for negotiate auth."); |
+ if (accounts.length == 0) { |
+ Log.w(TAG, "ERR_MISSING_AUTH_CREDENTIALS: No account provided for the kerberos " |
+ + "authentication. Please verify the configuration policies and " |
+ + "that the CONTACTS runtime permission is granted. "); |
nativeSetResult(mRequestData.nativeResultObject, |
- NetError.ERR_INVALID_AUTH_CREDENTIALS, null); |
+ NetError.ERR_MISSING_AUTH_CREDENTIALS, null); |
dgn
2016/01/06 21:13:24
Changed from INVALID to MISSING since we don't hav
|
return; |
} |
- if (!hasPermission(ContextUtils.getApplicationContext(), |
- "android.permission.USE_CREDENTIALS")) { |
+ |
+ if (accounts.length > 1) { |
+ Log.w(TAG, "ERR_MISSING_AUTH_CREDENTIALS: Found %d accounts eligible for the " |
+ + "kerberos authentication. Please fix the configuration by " |
+ + "providing a single account.", |
+ accounts.length); |
+ nativeSetResult(mRequestData.nativeResultObject, |
+ NetError.ERR_MISSING_AUTH_CREDENTIALS, null); |
dgn
2016/01/06 21:13:24
Changed from INVALID to MISSING since the credenti
|
+ return; |
+ } |
+ |
+ if (lacksPermission(ContextUtils.getApplicationContext(), |
+ "android.permission.USE_CREDENTIALS", true)) { |
+ // Protecting the AccountManager#getAuthToken call. |
// API < 23 Requires the USE_CREDENTIALS permission or throws an exception. |
- // API >= 23 USE_CREDENTIALS is auto-granted without having to be declared. |
- Log.e(TAG, "USE_CREDENTIALS permission not granted. Aborting authentication."); |
+ // API >= 23 USE_CREDENTIALS permission is removed |
+ Log.e(TAG, "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: USE_CREDENTIALS permission not " |
+ + "granted. Aborting authentication."); |
nativeSetResult(mRequestData.nativeResultObject, |
NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
return; |
@@ -119,7 +156,7 @@ public void run(AccountManagerFuture<Bundle> future) { |
try { |
result = future.getResult(); |
} catch (OperationCanceledException | AuthenticatorException | IOException e) { |
- Log.w(TAG, "Operation did not complete", e); |
+ Log.w(TAG, "ERR_UNEXPECTED: Error while attempting to obtain a token.", e); |
nativeSetResult(mRequestData.nativeResultObject, NetError.ERR_UNEXPECTED, null); |
return; |
} |
@@ -197,41 +234,12 @@ void getNextAuthToken(final long nativeResultObject, final String principal, Str |
HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, mSpnegoContext); |
} |
requestData.options.putBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE, canDelegate); |
- Handler uiThreadHandler = new Handler(ThreadUtils.getUiThreadLooper()); |
Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
if (activity == null) { |
- // This code path is not as able to get user input as the one that has an activity. It |
- // doesn't handle 0 or multiple accounts and will just result in a failure in those |
- // cases. |
- if (!hasPermission(applicationContext, android.Manifest.permission.GET_ACCOUNTS)) { |
- // API < 23 Requires the GET_ACCOUNTS permission or throws an exception. |
- // API >= 23 Requires the GET_ACCOUNTS permission or returns only the accounts |
- // whose authenticator has a signature matches our app. |
- Log.e(TAG, "GET_ACCOUNTS permission not granted. Aborting authentication."); |
- nativeSetResult(requestData.nativeResultObject, |
- NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
- return; |
- } |
- requestData.accountManager.getAccountsByTypeAndFeatures( |
- mAccountType, features, new GetAccountsCallback(requestData), uiThreadHandler); |
+ requestTokenWithoutActivity(applicationContext, requestData, features); |
} else { |
- // If there is more than one account, it will show the an account picker dialog for |
- // each query (e.g. html file, then favicon...) |
- // Same if the credentials need to be confirmed. |
- // If there is a failure, it will retry automatically. |
- if (!hasPermission(applicationContext, "android.permission.MANAGE_ACCOUNTS")) { |
- // API < 23 Requires the MANAGE_ACCOUNTS permission. |
- // API >= 23 Requires something in the CONTACTS permission group to behave properly. |
- // MANAGE_ACCOUNTS is auto-granted on install without having be declared. |
- Log.e(TAG, "MANAGE_ACCOUNTS permission not granted. Aborting authentication."); |
- nativeSetResult(requestData.nativeResultObject, |
- NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
- return; |
- } |
- requestData.accountManager.getAuthTokenByFeatures(mAccountType, |
- requestData.authTokenType, features, activity, null, requestData.options, |
- new GetTokenCallback(requestData), uiThreadHandler); |
+ requestTokenWithActivity(applicationContext, activity, requestData, features); |
} |
} |
@@ -281,11 +289,86 @@ private void processResult(Bundle result, RequestData requestData) { |
result.getString(AccountManager.KEY_AUTHTOKEN)); |
} |
+ /** |
+ * Requests an authentication token. If the account is not properly setup, it will result in |
+ * a failure. |
+ * |
+ * @param ctx The application context |
+ * @param requestData The object holding the data related to the current request |
+ * @param features An array of the account features to require, may be null or empty |
+ */ |
+ private void requestTokenWithoutActivity( |
+ Context ctx, RequestData requestData, String[] features) { |
+ if (lacksPermission(ctx, Manifest.permission.GET_ACCOUNTS, true /* onlyPreM */)) { |
+ // Protecting the AccountManager#getAccountsByTypeAndFeatures call. |
+ // API < 23 Requires the GET_ACCOUNTS permission or throws an exception. |
+ // API >= 23 Requires the GET_ACCOUNTS permission (CONTACTS permission group) or |
+ // returns only the accounts whose authenticator has a signature that |
+ // matches our app. Working with this restriction and not requesting |
+ // the permission is a valid use case in the context of WebView, so we |
+ // don't require it on M+ |
+ Log.e(TAG, "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: GET_ACCOUNTS permission not " |
+ + "granted. Aborting authentication."); |
+ nativeSetResult(requestData.nativeResultObject, |
+ NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
+ return; |
+ } |
+ requestData.accountManager.getAccountsByTypeAndFeatures(mAccountType, features, |
+ new GetAccountsCallback(requestData), new Handler(ThreadUtils.getUiThreadLooper())); |
+ } |
+ |
+ /** |
+ * Requests an authentication token. Handles the account selection/creation and the credentials |
+ * confirmation if that is needed. |
+ * If there is more than one account, it will show an account picker dialog for |
+ * each query (e.g. html file, then favicon...) |
+ * Same if the credentials need to be confirmed. |
+ * |
+ * @param ctx The application context |
+ * @param activity The Activity context to use for launching new sub-Activities to prompt to |
+ * add an account, select an account, and/or enter a password, as necessary; |
+ * used only to call startActivity(); should not be null |
+ * @param requestData The object holding the data related to the current request |
+ * @param features An array of the account features to require, may be null or empty |
+ */ |
+ private void requestTokenWithActivity( |
+ Context ctx, Activity activity, RequestData requestData, String[] features) { |
+ boolean isPreM = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; |
+ String permission = isPreM |
+ ? "android.permission.MANAGE_ACCOUNTS" |
+ : Manifest.permission.GET_ACCOUNTS; |
+ |
+ // Check if the AccountManager#getAuthTokenByFeatures call can be made. |
+ // API < 23 Requires the MANAGE_ACCOUNTS permission. |
+ // API >= 23 Requires the GET_ACCOUNTS permission to behave properly. When it's not granted, |
+ // accounts not managed by the current application can't be retrieved. Depending |
+ // on the authenticator implementation, it might prompt to create an account, but |
+ // that won't be saved. This would be a bad user experience, so we also consider |
+ // it a failure case. |
+ if (lacksPermission(ctx, permission, isPreM)) { |
+ Log.e(TAG, "ERR_MISCONFIGURED_AUTH_ENVIRONMENT: %s permission not granted. " |
+ + "Aborting authentication", permission); |
+ nativeSetResult(requestData.nativeResultObject, |
+ NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT, null); |
+ return; |
+ } |
+ |
+ requestData.accountManager.getAuthTokenByFeatures(mAccountType, requestData.authTokenType, |
+ features, activity, null, requestData.options, new GetTokenCallback(requestData), |
+ new Handler(ThreadUtils.getUiThreadLooper())); |
+ } |
+ |
+ /** |
+ * Returns whether the current context lacks a given permission. Skips the check on M+ systems |
+ * if {@code onlyPreM} is {@code true}, and just returns {@code false}. |
+ */ |
@VisibleForTesting |
- boolean hasPermission(Context context, String permission) { |
+ boolean lacksPermission(Context context, String permission, boolean onlyPreM) { |
+ if (onlyPreM && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) return false; |
+ |
int permissionResult = |
context.checkPermission(permission, Process.myPid(), Process.myUid()); |
- return permissionResult == PackageManager.PERMISSION_GRANTED; |
+ return permissionResult != PackageManager.PERMISSION_GRANTED; |
} |
@VisibleForTesting |